Accessibility

The floor, not the ceiling:

  • Every interactive target is at least 24×24px (WCAG 2.2 SC 2.5.8), and the templates hold to it with none left over. Check the width too — icon-only buttons are square. Where the visual is deliberately smaller — the 20px switch track, the 12px slider thumb, an 11px text link — the templates keep the visual size and grow the hit area with padding plus an equal negative margin, so the target reaches 24 without the row getting taller. --min-target is the value they all reference; change it in one place and every one of them follows.
  • Verify contrast per pairing, never assume it. Recompute the key pairings after a reskin, including the worst case of small text on the control fill. text-tertiary sits below AA by design, so it never carries a value, a message, or a label a decision rests on.
  • Keyboard reaches everything a mouse reaches, in visible order. Dialogs trap focus while open and return it to the trigger on close. A hand-built listbox owes the contract a native <select> gives away; see Components.
  • [hidden] needs restoring. Any component that sets display: flex or grid overrides the browser default [hidden] { display: none }. Ship [hidden] { display: none !important } in the base stylesheet — a collapsed panel leaking into view is usually this.
  • Meaning never rides on colour, shape or motion alone. A red border needs a message; a spinner needs text; a selected thumbnail needs its ring, not just a tint.

Key pairings, measured

PairingMeasuredThreshold
text-primary on white17.90:14.5:1pass
text-primary on bg-subtlest17.46:14.5:1pass
text-primary on bg-control15.56:14.5:1pass
text-secondary on white10.68:14.5:1pass
text-secondary on bg-control9.28:14.5:1pass
White on brand-default14.35:14.5:1pass
White on bg-inverse (the dark dialog header)17.90:14.5:1pass
stroke-surround (the focus ring) on white4.76:13:1pass
red-default as error text on white4.36:14.5:1borderline
text-tertiary on white3.60:14.5:1known exception
text-tertiary on bg-control3.13:14.5:1known exception
blue-default as link and info text3.79:14.5:1known exception
White on emphasize-default3.89:14.5:1known exception
text-disabled on brand-disabled1.57:1not meant to be read

The two steps that carry meaning — text-primary and text-secondary — clear the line on every surface in the system. The four rows marked as known exceptions are values the token export gives, not defects: they are listed again below with the reasoning. Recompute this whole table when you reskin.

Known and deliberately accepted deviations

A specification should record its own debts rather than pretend it has none:

ItemMeasuredThresholdReasoning
text-tertiary as helper text3.60:14.5:1 (1.4.3)The value the token export gives it. Tertiary therefore carries only text a reader can afford to miss — never a value, a message, or a label a decision rests on
blue-default as link and info text3.79:14.5:1 (1.4.3)The export's value. Links keep their underline, so colour is not the only signal
White on emphasize-default3.89:14.5:1 (1.4.3)The export's value for the platform red. It appears at most once per screen
text-disabled on brand-disabled1.57:1A disabled control is deliberately unreadable; the state is also carried by the cursor and by aria-disabled
stroke-default as a control's only boundary1.43:13:1 (1.4.11)Controls also differ from their surface by fill; the border is not the only cue
Grey control fill on a white card1.15:13:1 (1.4.11)The whole panel is built on this recess; focus adds a 1px stroke-active border, and the label says it is a field

The first four come from the token export and are accepted as they are — the way to honour them is to keep the affected text out of load-bearing roles, not to nudge the values. The last two are the source of the overall feel: darkening every border to 3:1 visibly heavies every edge, and giving a control a 3:1 boundary at rest brings back the bordered box the grey fill replaced.

Esc
Search all docs · to navigate · to open