Components
Arrival & Progress
The sibling of the error and empty state: that one stands in for content there will never be, this one for content that is on its way. A thing that has not arrived is drawn as itself, one rung down the presence ladder — its own contour at the 1-4 line, with one light crossing it. No grey slabs, no spinner, no script.
The plate reads twice
Line types catalogues the four lines the way the top half of the Formsprache > Linien plate does: four specimens side by side, equals, each with a job. The bottom half of the same plate is a different drawing and nothing here had implemented it. One object — a rhombus with an ellipse inside it — appears four times on one dotted axis, solid and small at the left, 1-4 and huge at the right.
Four line types, one figure. Read left to right it is not a specimen sheet at all: it is a thing coming into being, or going out of it. That is the second register the four types carry, and it only ever becomes legible when the same object is drawn more than once — which is why no static page has needed it and every state does.
assets/source/manual/shape-line-types.jpg — the register in the lower
half is the one this component implements.
| Rung | Token | Line | Means |
|---|---|---|---|
| 3 | --presence-present | solid | here. The object, drawn. |
| 2 | --presence-near | 2-1 | all but here — held for a state that is a claim about the object, not about the wait. |
| 1 | --presence-faint | 1-2 | construction: a line that explains the drawing rather than being it. |
| 0 | --presence-absent | 1-4 | not here yet. Every placeholder on this page. |
--presence-absent was already doing this
job under another name: .cf-plot__cell, the ghost footprint that says
where a column stands rather than what it measures. → tokens.css 8a
The ghost
A placeholder is the arriving thing's contour and nothing above it in the material stack: no fill, no light of its own, no text. A line of copy that is not here yet is an empty box the height of its own line — not a filled bar, which is a line of type that has arrived and is unreadable.
Beiträge werden geladen
<div class="cf-arrive"> <div class="cf-arrive__ghost" aria-hidden="true"> <span class="cf-arrive__plate" style="--arrive-h:9rem"></span> <span class="cf-arrive__line" style="--arrive-w:.94"></span> <span class="cf-arrive__line" style="--arrive-w:.88"></span> <span class="cf-arrive__line" style="--arrive-w:.52"></span> </div> <p class="cf-arrive__status" role="status">Beiträge werden geladen</p> </div>
Anatomy
| Part | Is | Notes |
|---|---|---|
.cf-arrive |
the block | Column, --space-3 between the drawing and the word. |
.cf-arrive__ghost |
the drawn half | Carries aria-hidden="true" and the travelling light. The status line is its sibling, not its child, for both reasons. |
.cf-arrive__line |
one line of type | Height is 1em × --leading-relaxed, so it tracks whatever type it stands in for. --arrive-w (0–1) gives copy its ragged right edge. |
.cf-arrive__plate |
a card, a photo, a figure | --arrive-h is the height the arriving thing will have. Give it the real one — a placeholder of the wrong size is a layout shift with extra steps. |
.cf-arrive__object |
an isometric object | The lattice's own 96 × 48 rhombus: the ghost of an illustration is the ground it will stand on. SVG, because a transform on a bordered box scales its contour. |
.cf-arrive__status |
the word | Mono label, role="status". Not optional — see below. |
The object
Where an illustration or a figure is arriving rather than a block of copy, the placeholder is the lattice cell itself.
Diagramm wird berechnet
<svg class="cf-arrive__object" viewBox="0 0 96 48"><path d="M48 0 96 24 48 48 0 24Z"/></svg>
The light
One band crosses the ghost at --angle-b, above the contour — the light
layer is layer 5 and the contour is layer 4, so this is the manual's own stacking
order and not a glow bolted on top. It is the one place in the system a continuous
loop is honest: something genuinely is still happening. The period is
--arrive-period, 2.4 s, twice the slowest duration the system otherwise
animates, because a fast loop reads as impatience.
--arrive-light-peak is therefore a decision about restraint — light stays
a moment and does not become a surface — with 0.52 as the ceiling it must never cross.
→ tokens.css 10
prefers-reduced-motion the band stops at 50 % — the
middle of the object, which is the same designed still the swinging foil and the
field's pool of light fall back to. The ghost stays: nothing is moving, and the thing
is still not here yet. In print the light goes and the ghost prints, because the ghost
is where the thing will be. In forced colours every background image is dropped, so
the placeholders are redrawn as borders — the only paint that survives.
Progress — the ladder, traversed
A wait with a known end is one line changing type as it is crossed: solid behind the head, 1-4 ahead of it, and the head itself the lattice cell filled with light. Nothing here is invented for the component — it is the plot's lit cap standing on the rail the ladder already describes.
Telemetriedaten werden importiert
42 %
<div class="cf-progress" style="--progress:.42"> <p class="cf-progress__label" id="prog-import">Telemetriedaten werden importiert</p> <p class="cf-progress__value">42 %</p> <div class="cf-progress__rail" role="progressbar" aria-labelledby="prog-import" aria-valuemin="0" aria-valuemax="100" aria-valuenow="42"></div> </div>
| Part | Is | Notes |
|---|---|---|
--progress |
the value, 0–1 | Registered with @property as a number, so a stray unit is the initial value rather than a rail drawn at full width. clamp() catches a 2 or a −1. |
.cf-progress__rail |
the line | Two background tiles, no overlay: the traversed run solid and pinned left, the untraversed run 1-4 and pinned right. The box is 8 px so the head has room; the line in it is 1 px. |
.cf-progress__rail::after |
the head | An 8 × 4 rhombus — the lattice cell at UI scale — filled lime with the plot's own glow. |
role="progressbar" |
on the rail, not the block | A progressbar's children are presentational: put the role on the block and the label and the value stop being readable. The rail is named by aria-labelledby pointing at the label. |
When the end is not known
A head that travels and resets is a claim about how far along the work is, made by
something that does not know. So the indeterminate rail has no head: it is 1-4 end to
end, and one segment of lit line passes along it — anchored at neither end,
entering off the left and leaving off the right, never growing from zero, so there is
no reading of it under which it is measuring anything. Drop aria-valuenow
with the head: the two say the same thing and only one of them is visible.
--gradient-foil-ink, the shadow
half of the foil — the one member of the family that is legible on this surface, since
every stop of the lit half is above OKLab L 0.82 and lands at about 1.1:1 on CF-Grau.
The band that crosses the ghost was tried here first and is wrong: spread at
--angle-b over an 8 px box it puts almost no ink on the one pixel that is
the line, and a wait indicator nobody can see is not one. Ghost line against lit line
is a difference in line type, which is the difference this whole component is
built on.
Bericht wird erzeugt
<div class="cf-progress cf-progress--indeterminate"> <p class="cf-progress__label" id="prog-export">Bericht wird erzeugt</p> <div class="cf-progress__rail" role="progressbar" aria-labelledby="prog-export"></div> </div>
Which one, and when
| Wait | Draw | Why |
|---|---|---|
| under ~1 s | nothing | A placeholder that appears and leaves inside a second is a flash, and the announcement arrives after the content it announces. Leave the space empty and let the thing land. |
| 1–10 s, shape known | .cf-arrive |
The reader can see what is coming and where it will sit, which is the whole benefit a placeholder has over a spinner. |
| 1–10 s, shape not known | .cf-progress--indeterminate |
One line and a word. Do not guess a shape — a ghost of the wrong thing is worse than no ghost. |
| over ~10 s | .cf-progress |
Past about ten seconds a reader stops waiting and starts wondering, and wants a gauge rather than a rhythm. |
What assistive technology gets
-
The ghost is decorative and is hidden.
aria-hidden="true"on.cf-arrive__ghost. Screen-reader support foraria-busyis thin enough that it cannot be the only signal, so the system does not lean on it. -
The word is the state.
role="status"— a polite live region — announces the wait without interrupting, and the same element carries the finished message when the content lands. A shape nobody can see is not a loading state. - Nothing here is focusable and nothing here is a control. A placeholder must never take a tab stop; it stands where content will be, and content is not yet there to be reached.
- The contour clears 3:1 at every frame, including the frames the light is crossing it. See the cap above.
- Reduced motion keeps the state and drops the loop. The ghost, the rail and the status line all survive; only the band stops.
Do / don't
| Do | Don't |
|---|---|
| Draw the placeholder at the size and position of the thing that is coming. | Draw a generic stack of bars. It is not a low-presence drawing of anything, and it moves the layout when the real content lands. |
| Fill the placeholder with nothing. Contour before fill. | Fill it grey and slide a lighter grey band over it. That is the loading state's version of the bar beside a block — it says "waiting" without saying anything about the object. |
Give the block one role="status" line. |
Ship the drawing alone, or put the status inside the aria-hidden half. |
| Let the head of a determinate rail be the screen's lime moment. | Put lime anywhere else in the block — not in the ghost, not in the label, not in the rail. |
| Use the indeterminate rail when the end is unknown. | Animate --progress on a timer to look busy. The value is a claim; only the thing doing the work may make it. |
| Replace the ghost with the object at the solid line. | Cross-fade the two, or leave the ghost under the arrived content. The ladder is a move between rungs, not a dissolve. |
Who sets it
Nothing in this system is dynamic, so no shipping page carries a state from this file
today — the same way patterns/news.html renders page 1 of 11 flat and the
server owns the paging. What the component fixes is the drawing and the contract, so
that whoever adds the behaviour has nothing left to decide:
-
The theme filter on the news overview. Swap the grid for
.cf-arrivewith one.cf-arrive__plateper card at the card's own height, and update.cf-arrive__statuswhen the answer lands. A filter that matches nothing is the other component — the inline error — at200. -
The contact form. A native POST navigates and needs none of this. A
form submitted in the background does:
.cf-progress--indeterminateunder the actions, and the submit button disabled while it runs. -
Anything with a byte count — an upload, an export, an import — takes
the determinate rail. Set
--progressandaria-valuenowin the same write, or the drawing and the announcement disagree.