Components
Search & Results
The surface the company is named after. The found state settled what a match looks like in all four registers the platform offers and then shipped none of them on a page, because the system had no page that answers a question. This is that page: a control, a register of answers, and one decision about where the light goes that the rest of the chapter had left open.
The control
A <form role="search"> around one .cf-field and one
button. No new field drawing: a search box is a text input, and the system already has
one — underlined, no box, mono label. Adding a second input style so this one could
carry a magnifier would be two drawings for one thing.
<form class="cf-search" role="search" action="/suche" method="get">
<div class="cf-field cf-search__field">
<label class="cf-field__label" for="q">Suchbegriff</label>
<input class="cf-field__input" type="search" id="q" name="q" value="Telemetrie"
autocomplete="off" spellcheck="false" enterkeyhint="search">
</div>
<button class="cf-btn cf-btn--solid cf-search__submit" type="submit">
Suchen
<svg class="cf-arrow" aria-hidden="true"><use href="#cf-arrow"></use></svg>
</button>
</form>
method="get", and it is not a preference. The query has to
end up in the address, because a result set that cannot be linked, bookmarked or reached
with the back button is not a page — it is a state that happened once. Every listing in
this system already works this way. The filter is the URL is the rule, and a
query is a filter — where a static file cannot answer one, the state gets an address of
its own instead: the News topic filter is a page per topic,
news-thema-<slug>.html, and never a query string nothing serves.
flex-wrap answers “does it still fit”
without adding a consumer to the
breakpoint register. That register
is a list to be added to deliberately, and two things in a row is not a reason.
The register of answers
An <ol>, because the order is a claim. Each result is a block of type
standing on a rule — no box, no fill, no corner, nothing that would make it a card. That
is the same claim the found state makes about a
marked word one scale down: an isometric object has no bounding box, what places it is
the lattice edge under it. A result is a paragraph standing on one.
Treffer
2 von 6-
Expertise — Telemetrie-Analytik und Datenintegration
Vier Felder, ein Problem: die Daten liegen an, nur nicht zusammen. Der Weg dahin ist in allen vier derselbe — vom Sensor bis zur Auswertung, ohne Bruchstelle in der Mitte.
control-f.de/expertise
-
Digitale Zwillinge im Energiesektor: Simulation trifft Realität
Das Datenfundament: Bevor irgendetwas simuliert wird, muss die Telemetrie stimmen. In den Projekten, die wir seit 2022 begleitet haben, lagen die Probleme fast immer davor.
control-f.de/news/digitale-zwillinge-im-energiesektor
Both links above are live. Follow either one and the destination page opens scrolled to the quoted phrase with that phrase lit — which is the whole point of the next section.
<ol class="cf-results" role="list">
<li class="cf-result">
<p class="cf-result__meta">
<span aria-hidden="true">01</span>
<span>Seite</span>
</p>
<h3 class="cf-result__title">
<a class="cf-result__link" href="/expertise#:~:text=Telemetrie%2DAnalytik">
Expertise — <mark class="cf-mark">Telemetrie</mark>-Analytik
</a>
</h3>
<p class="cf-result__excerpt">
Vier Felder, ein Problem: die Daten liegen an, nur nicht zusammen.
</p>
<p class="cf-result__path">control-f.de/expertise</p>
</li>
</ol>
.cf-blog-card:last-child { border-bottom: 0 }: that rule exists because the
grid container already draws the bottom edge, and this list has no container.
Contour here, light there
The found state has two rungs. Every match is contour — the ground line alone. One match is light — the same word with lime behind it — and the chapter is explicit that there is exactly one, ever, because it means the hit the reader is on.
On a result page the reader is on none of them. They have not chosen yet; that is what
the page is for. So .cf-mark--current has nothing to attach to and
a result set is drawn entirely in contour — six matches, six ground
lines, no light. The page's one lime moment goes where it goes on every other listing in
the system: the call to action in the footer.
#:~:text= fragment quoting the phrase in its own excerpt. Click one and the
destination page arrives scrolled to that phrase with it drawn in the lit rung — by
::target-text, which base.css has styled since the found state
shipped. The destination implements nothing. It only has to have drawn the state.
So the found state's two rungs are not two treatments of a word on one page. They are the question and the answer, one page apart: the register of candidates is contour, and the thing you were looking for lights up when you get there. That is the brand's own argument — data can be found in a space rather than read off a sheet — running across a navigation rather than inside a paragraph.
Encoding the fragment is the only fussy part, and it has one trap.
- is the delimiter for the prefix-,start,end,-suffix form, so a
literal hyphen inside the quoted phrase must be written %2D or the browser
reads half the phrase as a prefix and matches nothing —
silently, because a fragment that does not match simply does nothing. Space is
%20, and non-ASCII is UTF-8 percent-encoded as usual
(ü → %C3%BC).
| Phrase in the excerpt | Fragment |
|---|---|
| muss die Telemetrie stimmen | #:~:text=muss%20die%20Telemetrie%20stimmen |
| Telemetrie-Analytik und Datenintegration | #:~:text=Telemetrie%2DAnalytik%20und%20Datenintegration |
| Big Data-Plattformen für Telemetriedaten | #:~:text=Big%20Data%2DPlattformen%20f%C3%BCr%20Telemetriedaten |
Keep the quoted phrase to the shortest run that is unique on the target page. The
fragment matches the first occurrence, so a phrase that repeats sends the
reader to the wrong one, and a phrase long enough to span an element boundary the editor
later changes will stop matching. Four to six words is the working band. Where a phrase
cannot be made unique, the textStart,textEnd form
(#:~:text=Bevor%20irgendetwas,stimmen) pins both ends instead.
No answer
Zero results is not an error and must not be drawn as one: the search worked, the answer
is empty. It takes .cf-error--inline, the same component a listing renders
in place of its grid when nothing is left to draw, served at 200.
0 Treffer
Dazu steht hier nichts.
Für Rhombendodekaeder gibt es auf control-f.de keine Fundstelle. Ein kürzerer Begriff oder ein anderer Bereich führt weiter — die Suche schneidet nicht an Wortgrenzen ab, Telemetrie findet also auch Telemetriedaten.
Anatomy
| Part | Value |
|---|---|
.cf-search | flex row, --space-4 gap, aligned on the field's baseline rule; wraps below its own content width |
.cf-search__field | flex: 1 1 16rem — grows, and 16 rem is the basis at which a query is still readable |
.cf-search__submit | .cf-btn--solid. The one filled control on the page; its label is the foil, not lime |
.cf-results | 1 px top rule in full ink — or none, when the section header above carries --flush and supplies it |
.cf-result | --space-6 (24 px) above and below, 1 px bottom rule in full ink, no box |
| Result on hover or focus | --sheen-panel travels across, 480 ms — the same move the blog card and the accordion row make |
.cf-result__meta | Geist Mono, 11 px, uppercase, tabular, --text-secondary |
| Ordinal | two digits, aria-hidden — the <ol> already carries the position |
.cf-result__title | Publica Sans, --text-h4 (20 px), regular, --leading-normal |
.cf-result__link | no underline at rest; 1 px underline at 0.2em on hover and :focus-visible |
.cf-result__excerpt | 14 px, --leading-relaxed, capped at --measure-prose (53 ch), --text-secondary |
.cf-result__path | Geist Mono, 11 px, --text-secondary, truncated at the end with an ellipsis |
| Matches | .cf-mark throughout — contour rung, 2 px ground line. Never .cf-mark--current |
| Lime on the page | none in this component. It is spent on the footer CTA |
Behaviour
-
A script, because there is no server. This section shipped saying
“no script — the form is a GET, the server renders the answer, and the page is a
page”, which is the right design and describes a site this is not. Every address
here is a static file on GitHub Pages; the one route with a server behind it is the
contact form's
POST. So nobody was rendering the answer, and /suche drew the same six results for “Telemetrie” whatever was typed into it, for the whole life of the section.assets/js/cf-search.jsis what renders it now. -
The answer is computed where the pages are, not in the browser.
scripts/build-search-index.pyreads the shipped HTML afterbuild-site.pyhas written it — so the index carries the address the reader will actually be at, not the pattern's — and writes one file per edition underassets/search/. The script fetches, matches and draws. No page is ever parsed in a browser, and nothing walks the DOM of the page it is on. -
The script marks the matches, and it marks them as nodes. The
objection to marking after the fact was about injecting into text a screen reader is
already reading; nothing here does that. The register is built off-document and put
in whole, with
<mark class="cf-mark">created as an element and the copy around it as text nodes — never a string of markup, because the index holds page copy and the query is the reader's, and that is the one injection this component could ever have. -
The page ships a worked answer, and it is a fallback rather than a
specimen. With scripting off,
/sucheis six real hits for a real query with that query in its field. With scripting on, the script writesdata-cf-searchon<html>from the<head>— before the body is parsed, the same trickcf-nav.jsuses — andcomponents.csskeeps the claim and the register off the glass until the reader's own answer replaces them. -
Every sentence the script draws lives on the page. The copy is a
<template>of[data-key]strings plus one template per block state, sodesign-system/i18n/en.jsonstill owns the words andbuild-i18n.pystill translates them. A sentence inside the script would be a sentence the catalogue could not see and the extractor could not miss.scripts/check-search-contract.pyholds both directions: a key the script asks for and the page has not got, and a string on the page nothing reads. -
Escape the query before echoing it. It appears in three places — the
input's
value, the page<title>and the header meta — and every one of them is reader-supplied text going back into markup. This is the one component in the system where that is true. -
Thirty results, and the page says so when it caps.
Pagination's own rule is that it joins above about four
screens, and thirty is about there. A query broad enough to pass it is a query to
narrow rather than a ranking to read down, so the register states the cap in a line
under itself instead. Pagination is still the answer if that changes, and the step
links then carry the query as well as the page —
?q=…&seite=N, or the term is lost on the first step. -
Nothing animates but the sheen. 480 ms across the result, which drops
to 1 ms under
prefers-reduced-motionthrough the duration token, with no rule of its own. → Motion
Accessibility
role="search"on the form. It is the landmark for this, and it is the only landmark a search form should carry.- A real
<label>, not a placeholder. A placeholder disappears the moment the reader types — which on this page is always, because the query is already in the field. type="search": the correct role in the accessibility tree, the browser's own clear control, and a magnifier on the soft keyboard.enterkeyhint="search"relabels the return key.- An
<ol>withrole="list"—list-style: nonedrops the role in Safari, and the count on entry is the fastest way to hear how big the answer is. - Each result's heading is inside the list item, so heading navigation walks the answers. The ordinal is
aria-hidden: the list already carries position, and a decorative counter read aloud is noise. - A live region on the count, and this section used to argue the opposite. It argued it correctly, for a server: a fresh response needs no announcement, its
<title>carries the number, and a live region already in the DOM at load is not announced anyway. It then named the exception — “a variant that swaps results in without navigating does need one, rendered empty first, then filled” — and that variant is what ships. The claim span carriesrole="status", the register is emptied before it is filled, and the<title>is rewritten with the same number. - Contrast:
--text-secondaryon the page wash is 5.9:1, result titles are full ink, and the match's ground line is--border-strong. No text on this page sits on lime, because no match here is lit. - The sheen is a background travel behind type that never changes colour, so every figure above holds at every frame of it.
Rules
Do
- Put the query in the URL and echo it back in the field, the title and the header.
- Mark hits with
<mark class="cf-mark">, server-rendered, in the title and the excerpt both. - Give every result a
#:~:text=fragment quoting its own excerpt, four to six words, hyphens as%2D. - State the address of each answer. A ranking the reader cannot check is a ranking they have to trust.
- Draw zero results with
.cf-error--inlineat200, quoting the term that failed.
Don't
- Never light a match on a result page. There is no current hit until the reader chooses one, and a lit candidate is a page pretending to have decided.
- Don't make a result a card. No box, no fill, no corner — it stands on a rule like everything else in this system.
- Don't intercept Ctrl+F to open this form. The browser's find belongs to the reader; this is an addition to it. → The found state
- Don't let the search page be indexed.
noindex, follow— the content lives on the pages it points at. - Don't quote a phrase in the fragment that repeats on the target page. It matches the first occurrence and fails silently.
- Don't add a second input style for search. The system has one field drawing.
Where it lands
The Suche pattern is the whole page — header, control, six results, no pagination, the footer taking the page's one light. 404 routes to it, because a reader who has arrived at a page that does not exist is exactly the reader with a query.
417 × 41, and a seventh item is a change to
the drawing rather than to the code. It left the decision open — “nav, footer, or
neither” — and the answer is both. Search is the seventh route, last in the
list and before the language switch, because it is the one route that is a way of
finding the others; the switch stays last because it is the one control that moves you
between editions rather than through the site.
It is a label on the plate and not a control with a contour, and that follows the bar's own doctrine rather than taste:
/suche is a page, the field is on it, and
a search box in the chrome would be the second field drawing this system has spent the
whole chapter refusing. Measured at 1280 px: the plate goes from 733 px to 802 px and
its height does not move — 70 px, which is the label at --text-xs plus
the list's --space-1 gap. The plate is max-content under a
max-width: 100%, so nothing in the drawing had to move to make room, and
the folded bar below 780 px carries the seventh link in the panel like the other six.