Accessibility

Every number on this page is computed as the page renders, by the same function the test suite runs. None of them was typed in and hoped over.

Most design systems state a claim — AA compliant — once, by hand, near the top of a page. The claim is true when written and quietly stops being true the first time a value moves. Here the documentation and the tests read the same source, so a token cannot drift without both the page and the build changing.

Worst case, not best case

A colour that clears AA on a white card and fails in a table header has not cleared AA. Every figure below is the lowest ratio the token reaches against any surface it can sit on, and the surface that produced it is named underneath.

Text

Against all four surfaces, in both themes. The threshold is 4.5:1.

TokenLight, worst caseDark, worst case
text/primary13.86Pass
on sunken
15.17Pass
on overlay
text/secondary10.46Pass
on sunken
9.47Pass
on overlay
text/tertiary4.58Pass
on sunken
6.81Pass
on overlay
text/accent5.07Pass
on sunken
9.23Pass
on overlay
text/success6.58Pass
on sunken
9.85Pass
on overlay
text/warning10.43Pass
on sunken
9.51Pass
on overlay
text/danger7.54Pass
on sunken
9.15Pass
on overlay
text/info6.69Pass
on sunken
9.83Pass
on overlay

Control borders

WCAG 1.4.11 asks for 3:1 where a border is what identifies a control. border/strong is the only value in the ramp that manages it against every surface in both themes, which is why every form control uses it and why it is the same primitive in light and dark.

TokenLight, worst caseDark, worst case
border/strong3.04Pass
on sunken
4.56Pass
on overlay
border/focus3.34Pass
on sunken
9.23Pass
on overlay
border/danger5.05Pass
on sunken
6.35Pass
on overlay

Button labels

Measured against every fill the label can appear on, not just the resting one. In dark the accent fill lightens across hover and pressed while the label darkens to compensate — holding the label at white would put the hover step under AA.

TokenLight, worst caseDark, worst case
interactive/on-accent5.96Pass
on accent
7.73Pass
on accent
TokenLight, worst caseDark, worst case
interactive/on-danger5.93Pass
on danger
7.58Pass
on danger

Where the system falls short, on purpose

Four pairs sit below 4.5:1, all four deliberate. Listing them is the point: a system that reports no exceptions is a system that has not looked.

TokenLightDarkWhy
text/placeholder4.586.81Placeholder text is a hint, never the only copy of a label. Darkening it far enough to clear AA makes an empty field read as a filled one.
on-success on success-pressed11.8214.53Pressed is feedback after the decision, not information used to make it — nobody reads a label while their finger is down. The green ramp has no third step that keeps a dark label above 4.5, and a light label fails far worse.
text/disabled2.043.03WCAG 2.1 exempts inactive components. Low contrast is what communicates that the control cannot be used.
text/inert on surface/overlay1.721.97The calendar’s days from the adjacent months: not focusable, not clickable, their numbers hidden from a screen reader. Decoration is exempt. Kept quieter than text/disabled in both themes, so a day outside the month never reads as more present than an unavailable one.

The first three are asserted in the test suite at their recorded figures, so an edit that makes any of them worse still fails the build; text/inert is asserted as quieter than text/disabled in both themes. An exemption is not a place to stop measuring.

What the suite actually checks

The suite runs before anything deploys, and blocks it on failure. In plain terms:

  • Every text token clears AA on all four surfaces, in both themes.
  • Every status colour clears AA on its own subtle background, not just on white.
  • Every button label clears AA on all of its fill states, not only at rest.
  • Control borders clear 1.4.11 on every surface they can appear on.
  • The dark elevation ladder stays ordered, and each step stays separable.
  • No token collapses into the surface behind it.
  • Both generated stylesheets stay in step with the token source.
  • Components keep their semantics: labels, roles, keyboard, form participation.
  • Every page of this site passes axe at WCAG A and AA, and the states a page cannot show — an open menu, an open date picker, a field in error — are checked beside their component.

The colour checks caught five real defects the first time they ran. One was a divider that resolved to exactly the same value as the surface beneath it — 1.00:1, invisible, and not something anyone finds by eye in a palette of eighty-one greys.

Beyond colour

Contrast is the part that can be measured, which makes it the part that gets measured. The rest is enforced by component tests instead.

Focus adds geometry. A ring is drawn outside the control rather than the border being recoloured, so colour is never the only channel carrying the state. A field’s border keeps its width in every state, so nothing reflows when it gains focus.

Error survives focus. Focusing an invalid field adds the ring and keeps the red border. One signal does not replace the other.

Controls cannot ship unlabelled. Checkbox and Radio wrap their input in the label, so there is no id to forget. Input and Textarea deliberately do not invent a label — a control that guesses at one gets it wrong.

Descriptions stay descriptions. Help text sits outside the label and is attached with aria-describedby. Inside it, assistive technology reads it as part of the control’s name.

Everything is a real element. Native inputs underneath mean arrow keys move through a radio group, space toggles a checkbox, and form submission works without a single key handler.

Motion asks first. Every transition is removed under prefers-reduced-motion, and what it carried stays: the switch’s knob still moves, it stops sliding. The one animation, the Loader’s spin, slows rather than stops, because a frozen spinner reads as a hung page.

Targets have a floor. No control is under 24px, the AA target size, and the ones that are close borrow the rest from what wraps them — a checkbox from its label, a row’s checkbox from the row. The small button is 32px; for a screen that is touched, the large one is 48.

Keyboard and structure

Every component has a keyboard path, and it is the platform’s wherever the platform has one: Space and arrows on the native inputs, Enter on a button, Esc on a popover. Where the pattern asks for more, it is the APG’s — one tab stop and roving arrows in the menu and the calendar grid, Tab wrapping inside an open picker, Esc handed to the caller by the dialog rather than closing behind its back. A modal makes the page behind it inert and returns focus to what opened it; a closing menu does the same.

The site is built the same way. A skip link is the first tab stop, the page is a main landmark with the navigation, the section list and the pager as named navs, every page has one h1 and a heading outline with no skipped level, and every section heading is an anchor. The page reflows to 320px and to 200% zoom with no sideways scroll.

What a product still has to do

The system carries what it can: the ratios, the roles, the focus, the keyboard. A product built on it still owns the parts only it can know.

  • Name every icon-only button for its action — Add attachment, not the glyph — and every image for its meaning, or mark it decorative.
  • Write the error: what to enter, beside the field that failed, with focus moved to the first one on submit. The components mark and announce; the words are the product’s.
  • Keep one h1 per page and an outline underneath it, and put a skip link and a main before the chrome, as this site does.
  • Announce what changes away from the control that changed it: a polite status region for a toast or a result count, alert only for an error the reader must hear now.
  • Give a field its type, inputMode and autoComplete, and never block paste.
  • Keep hit areas apart in a layout of your own, and never let a decorative layer sit over a control without pointer-events: none.