Decisions

The parts of the system that look like mistakes, and the measurements that made them the right answer.

Each entry is a record: what was decided, the number that decided it, and the date where the record has one. An entry with no date is from the first drawing. A decision that runs against a good instinct — a border that fails a guideline on purpose, a ladder that is not symmetric — is written here so the next person finds the number before they find the instinct.

Light and dark are not symmetric

In light, surface/raised and surface/overlay are both white; a shadow separates a card from a modal. In dark, shadows stop reading as elevation, so overlay has to be a lighter colour step instead. The two modes describe the same hierarchy through different means, and flattening that into symmetry breaks the dark theme.

Button labels are themed, not constant

In dark the accent fill lightens across hover and pressed, while its label darkens to compensate. This is the trap that makes the palette look incapable of a lighter dark-mode hover: hold the label at white and the hover step lands at 1.77:1, nowhere near AA, with no lighter violet available that fixes it. The label was never the constant.

on-accent over accent, hover, pressed — dark: 7.73AAA 11.02AAA 13.99AAA

The highlight button lightens on hover, in both modes

interactive/tertiary resolves to flare, the gold the peaks take before they turn pink. Its fill is flare/400 with a dark label. The step below it, flare/500, is the stop where no label passes at all.

Rejected: flare/500 as the hover fill.

with stone/900 — 4.293:1 · with white — 3.803:1

So the only three-step ladder that passes goes the other way: 400, then 300, then 200, the label staying dark throughout. Light and dark share it, because the rule that produced it is the same in both — the accent already lightens in dark for the same reason. The tone is withheld from the outline and ghost variants, enforced in the type signature: the theme has no flare text colour, and nobody has drawn the button that would need one.

Checkboxes take the strong border; text fields have none at rest

Decided 2026-09-07, and the code has since moved

border/strong is the same primitive in both modes, which is unusual. It is the only value in the ramp clearing 3:1 against all four surfaces in light and dark, so a checkbox and a radio — where the border is the control — use it without a per-surface exception. Sitting mid-ramp, it contrasts in both directions.

The text field is the exception, and it has two records. The decision of 2026-09-07 kept a hairline in border/default at rest, 1.40:1 in light and 1.53:1 in dark, failing 1.4.11 to preserve the drawn look. The code today draws no resting border at all — the boundary is the fill, 1.07:1 against a card in light — and the Input page records that as a known gap. The two have not been reconciled, and this entry says so rather than picking one quietly.

The dark ramp holds three elevation levels, and sunken shares the canvas

Decided 2026-09-11, with the deep tail

Base, raised, overlay — 950, 925, 900 — and then it is full. The ramp ends at 950, so in dark surface/sunken is the canvas: a well reads as recessed inside a card, and on the canvas it takes a border. A dropdown opened inside a modal stays on surface/overlay and is separated by a border rather than another fill step. A twentieth step was measured and refused — adjacent steps of the neutral it would have extended were 1.08 to 1.23:1 apart, which is how a system ends up with two text levels nobody can tell apart. Running out is not a flaw to design around; it is a constraint to state plainly so nobody invents a level that collides with something.

925 is a surface step, and the only half step there will be

Decided 2026-09-12

The ladder used to be 950, 900, 800: one whole stop per level, ΔL .085 in OKLCH, and it looked strong because it was. Every reference system measured — Radix, Atlassian, Spectrum, Geist — places adjacent surface levels at .025 to .045. The rule against half steps was written against text levels nobody could tell apart, and no text is ever set in one surface against another, so one stop was added for surfaces and nothing else: 925 at L .205, in night and stone only, the two families a surface ladder is built from. The steps are now 0.043 and 0.043. The 975 the old rule refused stays refused. See Elevation and states for the survey.

Surfaces are measured in lightness, not in the contrast ratio

Decided 2026-09-12

The WCAG ratio adds 0.05 to both luminances, which flattens the dark end: Radix’s first two dark greys are 1.06:1 apart and everyone sees the step. This system’s new step is 1.08:1, and the suite’s old floor of 1.09 would have refused it while passing the jump that looked wrong. Text on a surface and a boundary on a surface stay on the ratio, which is what WCAG asks of them. A ladder step, a well inside a card and a wash over a surface are measured in OKLCH lightness, floor .035.

Hover is a wash, not a fill

Decided 2026-09-12

A row, a menu item, a ghost button and the neutral button all used to hover to one opaque colour: mist/100 in light, stone/700 in dark. Over a dark card that was ΔL +.184 where the references sit at +.05 to +.09, and in light it was invisible on surface/sunken, the same primitive. Now they take interactive/wash-hover, mist/500 at 8% in light and 12% in dark — one ink that darkens a light surface and lightens a dark one, laid over whatever is beneath. A hovered row on a dark card is 0.057 above it. The text beneath keeps its own token and is measured there: the tightest pair where rows live, text/tertiary on a hovered row on a light card, is 4.95AA. The filled ladders — accent, danger, success, tertiary — are untouched: a filled button changes its own colour, and its label is themed with it.

The menu takes a border in dark and not in light

Not an oversight of symmetry. Against the ground it falls on, the shadow reaches 1.19:1 in light at 8% opacity and 1.05:1 in dark at 64% — in dark it has stopped carrying elevation, whatever it is set to. The border is what separates the menu there, and against the canvas it is 3.55:1 in dark against 1.60:1 in light: stronger where it has to be. In light the shadow already does the work, and drawing the edge twice would look like a mistake, because it would be one.

Each menu tone hovers to its own fill

The drawing gives every row the same hover fill. Two measurements broke that. The drawn fill is surface/base, which in dark is darker than the menu — the row under the pointer would open a hole rather than light up. And the accent row’s label on a shared neutral fill was 3.50:1 in dark when the rule was made. The deeper dark tail brought it to 4.59:1, which passes, and the rule stays: a tone hovers to its own subtle surface because that is the design, not because of 0.09 of headroom. On its own surface the label is 9.64:1, and on the wash the neutral row takes now it would be 7.86:1.

The menu takes the top layer, and the suite cannot see it

Decided 2026-09-10, checked by hand in Chrome and Safari

jsdom, which runs the test suite, implements none of the popover API. The menu uses it anyway. A menu opens from inside other components — a row of actions in the Table, whose scroll container clips anything positioned inside it — and escaping that clip is the reason for an overlay to exist. The suite stubs the calls the component makes and asserts everything the component decides: roles, rows, keys, disabled rows, the stylesheet. Esc, the outside click, focus return and placement belong to the browser. All four were checked in Chrome, and this entry says so rather than letting a stub claim them.

The section list joins the prose at 1440, and not before

Decided 2026-09-12

The site’s page has three columns on a wide screen — the sections, the prose, the measurements — and a Table specimen needs 704px of prose to keep its header: 654 for the table and a specimen’s padding and hairline on both sides. At 1440 the prose has 728 with the list beside it; at 1280 it would have 616 and every Table on its own page would render collapsed. So below 1440 the list sits at the head of the measurements column instead, and the prose keeps 784.

The Dialog’s title wraps

Decided 2026-09-13

It used to truncate with an ellipsis, and at xs the header’s grid left it 112px: two 40px buttons, two 40px gaps, and 48 of padding out of 320. Reschedule appointment, this system’s own example, read “Reschedule appoint…”, and the question a confirmation asks was cut at “Cancel th…”. A title the reader cannot read is a dialog they cannot answer, so the title wraps, balanced, and the gap came down to 24 — the header’s own padding — because “appointment?” alone is 115px. A stylesheet test refuses the ellipsis from now on.

A theme flip holds every transition but the toggle’s own

Decided 2026-09-12

A flip changes the colour of nearly everything at once, and every colour transition on the page fired together: measured on the Button page, 92 on the buttons and 7 on the section list, so the buttons faded for 120ms while the page around them snapped. The toggle now puts an attribute on the root for the frame the theme is written in, the stylesheet turns transitions off under it, and the next frame takes it away. The toggle’s own transitions are excepted, because the knob’s slide is the control answering the click. Measured after: four transitions, all the toggle’s.