Components
Subdivision Field
The manual's Teilungsraster plate makes a claim the web implementation had
never answered: the recursive subdivision “kann als Konzept für interaktive
UI-Elemente oder Menüstrukturen genutzt werden.” Until now the subdivision was a
layout — .subdivide, and the blog grid on top of it — where the
hierarchy is authored once and then fixed, because which article is largest is a fact
about the archive. A field is the same series with the hierarchy
handed to the reader. Point at a cell and it takes the half; everything else keeps
its place in the row and continues the series behind it.
The field
Five cells, resting. This is the pure plate drawn exactly — 1/2, 1/4, 1/8, 1/8 — because the resting head is the first cell. Move across it, or tab into it.
The tick names its cell at every width, which is the only thing a 1/16 cell has room for and is how the application plate labels a column too. The copy arrives in the same series the widths do: the head carries all of it, the next rank keeps its title, and from there down the tick is the cell.
<nav class="cf-subdiv" aria-label="Prozess"> <a class="cf-subdiv__cell" href="/prozess/discovery"> <span class="cf-subdiv__tick">01 — Discovery</span> <span class="cf-subdiv__body"> <span class="cf-subdiv__title">Wir hören zu, bevor wir messen</span> <span class="cf-subdiv__meta">Workshop · 2 Wochen</span> </span> </a> <!-- three to six cells; nothing else is authored --> </nav>
:has(> .cf-subdiv__cell:nth-child(5):last-child)
sets --subdiv-n — so the count cannot drift from the number of cells the
way a hand-written --subdiv-count can. Index comes from
:nth-child. The only number that moves is the head, and it is one
inherited value on the container. Add or delete a cell and the series re-forms
correctly with no other edit. Compare .subdivide, where
--rank and --subdivide-count are both hand-set: that is
right for a layout, where the ranks encode editorial fact, and wrong here.
The rule, and why it is this rule
The halving series never changes: 1/2, 1/4, 1/8 … with the last two ranks
tied, which is what makes the fractions sum to exactly one rather than to one minus
the tail. What changes is who stands at its head. The head takes rank 0; every
other cell keeps its position in the row and takes the next rank in markup order.
| Head | Widths, five cells | Where the rules stand |
|---|---|---|
| cell 1 (rest) | 16 · 8 · 4 · 2 · 2 | the pure plate |
| cell 2 | 8 · 16 · 4 · 2 · 2 | rules 3, 4, 5 unmoved |
| cell 3 | 8 · 4 · 16 · 2 · 2 | rules 4, 5 unmoved |
| cell 4 | 8 · 4 · 2 · 16 · 2 | rule 5 unmoved |
| cell 5 | 8 · 4 · 2 · 2 · 16 | — |
Three properties fall out of that one rule, and together they are the argument for it over the obvious alternative — rotating the series so the head always sits in the widest slot.
2. The head only ever grows around the pointer. Cell k at the head takes
s1 while cells 1..k-1 take s2..sk;
the series is non-increasing, so sum(s2..sk) ≤ sum(s1..sk-1) and the
head's leading edge moves left and never right. Its box strictly contains the box it
had, so the pointer that summoned the cell is still inside it.
The rotation fails exactly here. Measured in Chromium at 1024 px:
hovering cell 4 under a rotating series moves that cell from
[896, 960] to [256, 768] — it leaves the pointer entirely,
the cursor lands on cell 5, and the field jumps a second time. Under this rule the same
cell goes from [896, 960] to [448, 960]. A layout that moves
out from under the pointer that summoned it is a trap; this one cannot be one.
3. The rules ahead of you do not move. Cells after the head keep their resting weights exactly, so every hairline from the head rightwards stands where it stood at rest. Only the rules behind your attention slide. That is what makes a field of six moving columns legible rather than busy: at any moment at most half the drawing is in motion, and it is the half you have already read.
Written in CSS the rank is four lines, because CSS has no conditional and
clamp(0, x, 1) is the sign of an integer:
--subdiv-after: clamp(0, var(--subdiv-h) - var(--subdiv-i), 1); /* 1 if this cell precedes the head */ --subdiv-before: clamp(0, var(--subdiv-i) - var(--subdiv-h), 1); --subdiv-eq: calc(1 - var(--subdiv-after) - var(--subdiv-before)); /* 1 only at the head */ --subdiv-r: calc((var(--subdiv-i) + var(--subdiv-after)) * (1 - var(--subdiv-eq))); flex-grow: pow(2, calc(var(--subdiv-n) - 1 - min(var(--subdiv-r), var(--subdiv-n) - 2)));
The min() is the tie between the last two ranks. Swept in Chromium against
the shipping CSS — every depth from three to six, every head at each depth, 18 states —
inside a 1024 px field: the cells divide the field's 1023 px content box (the missing
pixel is its right contour) into 511.5 / 255.75 / 127.875 / 63.938 / 31.969,
exact halves at every rank, summing to the content box with no remainder in any state.
The containment property in point 2 was measured separately and holds for every cell at
every head.
Depth
Three to six cells, the same range .subdivide covers. Below three there is
no series; above six the tail cell is 1/32 of the field and the tick stops fitting.
Nothing is set to choose the depth — the field counts its own children.
This is the application plate's own subject — a blog index divided by recency — with the difference that the plate fixes the hierarchy and the field lets the reader move it. Four cells here rather than five, and neither the CSS nor the markup was told.
Anatomy
| Part | What it is |
|---|---|
.cf-subdiv |
The field. Carries top, right and bottom edges; the left edge belongs to the first cell. Sets --subdiv-height (14rem) and holds the head as --subdiv-h. |
.cf-subdiv__cell |
One cell, and a real link. Draws its own leading rule as ::before, so the rules are cell-owned and never doubled. |
.cf-subdiv__cell::after |
The light. The head's contour drawn in --gradient-foil-ink, faded in and out by rank rather than switched by a selector. |
.cf-subdiv__tick |
Geist Mono, vertical, standing on the leading rule. Visible at every rank — this is the cell's permanent name. |
.cf-subdiv__title |
Visible at ranks 0 and 1. |
.cf-subdiv__meta |
Mono, visible at rank 0 only. |
--subdiv-height |
The one thing worth overriding. 14rem by default; 9rem reads as a bar rather than a board. |
Where it sits in the six layers
Base wash, then contour, then light, then text — no opaque layer and no glass. The field is drawn, not filled, which is the default for anything structural in this system, and there is nothing complicated underneath it for glass to calm.
--gradient-foil-ink — the foil's shadow half
— instead of ink. This is the one place in the six-layer stack the light layer and the
contour layer are allowed to be the same pixel, and it is what makes the light appear
to travel along the field as attention moves. It is the ink half because the field sits
on the page wash, where the lit half of the foil measures 1.1–1.5:1.
It is never lime.
#E1FF00 on CF-Grau is 1.4:1 and a
contour has to clear 3:1 on its own. This is the same reasoning that put the foil
rather than the lime ramp in the black button's label: the foil exists precisely so
colour can enter a mark without spending the screen's one lime moment.
Behaviour, support and the ways out
| Path | What the reader gets |
|---|---|
| Pointer | Hover promotes the cell. The head's box always contains the box it had, so the pointer cannot fall out of the cell it opened. |
| Keyboard | Identical. :focus-visible is in the same :is() as :hover, so tabbing through the field opens each cell exactly as pointing at it does, and the focus ring is the system's own. |
| Touch | The resting field, and a tap follows the link. Every cell is named by its tick at rest, so nothing is reachable only by hovering. |
| Screen reader | A <nav> of links. Copy is faded with opacity, never display, so a cell's title is part of its link name at every rank — the accessible name does not depend on the visual state. |
| Reduced motion | Every state, no transition. The promotion still happens; it arrives instantly. Motion is opted into under prefers-reduced-motion: no-preference, the way the rest of the system does it. |
No :has() or no pow() |
Equal cells, all copy visible — the manual's other form, .subdivide--even, rather than a degradation. The whole system is inside one @supports selector(:has(*)) and (flex-grow: pow(2, 3)). |
| Under 44rem of its own width | The field folds to a stack: equal cells, every cell open, ticks horizontal, leading rules become top rules. A container query, not a media query — the same field is comfortable in 900 px of page and impossible in 900 px of viewport behind a 272 px sidebar. |
| Print, forced colours | Everything, unweighted and unfaded. |
flex-grow and not grid-template-columns.
A grid would put the whole track set on the container, which means one declaration per
head per depth — sixteen of them for depths three to six — where the flex form is one
declaration on the cell and a number that inherits. flex-grow is also a
plain number, so it interpolates: the field can transition where a track list cannot.
Why nothing animates to an intrinsic size.
interpolate-size: allow-keywords and calc-size() would be the
obvious modern way to open a cell to its content width, and they are still Chromium-only
— not Baseline in Firefox or Safari. A field whose proportions only resolve in one engine
is not a system. The series is unitless and needs neither.
What transitioning
flex-grow actually costs. It reflows the
field every frame — flex-grow is not composited, and no amount of it is. It
is affordable here because the field is one flex line of at most six items and the copy
inside them is faded rather than re-wrapped, so the reflow is a handful of boxes and no
text layout. This is not a licence to transition flex-grow on a page-level
layout.
Do and don't
| Do | Don't |
|---|---|
| Use it where the items have a natural order and unequal weight — a process, an archive by recency, a set of chapters. | Use it for items of equal rank. A subdivision claims a hierarchy; four services that matter equally should be four equal cells, and that is .subdivide--even. |
| Give every cell a tick short enough to read vertically in a 1/16 cell — two or three words. | Let the tick wrap. It is white-space: nowrap, so a long one will overflow its cell rather than fold. |
| Let the resting head be the cell you would have made largest anyway. It is always the first one. | Reorder the markup to change what opens first. Markup order is reading order, and the reader will find the rest. |
| Keep it to one per page. | Put two fields in one section. Two of them re-proportioning near each other is the busiest thing this system can draw. |
| Let the cells be links or buttons — something focus lands on. | Make the cells <div>s and drive the field from hover alone. That is a menu no keyboard can open. |
| Nest it in nothing. It is a band, full width of its column. | Put it inside the 12-column grid alongside content. The subdivision orders quantity; the 12-column grid orders content; the two are never nested. |
Still open
-
It is not on a designed page yet. The two mockup-derived pages —
Landing Page and Über uns — are drawn from Figma frames that contain no field, and
putting one in would be inventing a section the designer did not draw. The honest
first application is
patterns/news.html, whose archive is already eleven pages deep and whose topic row is three flat tags today; the manual's own application plate is a blog index divided exactly this way. That change needs the real per-year counts, which the page does not have, so it is proposed here rather than made. -
The head does not persist. Releasing the pointer returns the field
to its resting state. A field used as a section switcher would want the chosen cell
to stay open, which is
:targetor a radio group and no new geometry — but it is a different component's job, because a switcher owns a panel and this one owns only itself. -
Depth stops at six. Seven cells would need one more
:nth-childrule and a tail cell of 1/64.sibling-index()andsibling-count()would collapse all twelve index and depth rules to two declarations and remove the ceiling entirely; they are Chromium-only as of this writing, so the rules stay written out.