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.
| Token | Light, worst case | Dark, worst case |
|---|---|---|
| text/primary | 13.86Pass on sunken | 15.17Pass on overlay |
| text/secondary | 10.46Pass on sunken | 9.47Pass on overlay |
| text/tertiary | 4.58Pass on sunken | 6.81Pass on overlay |
| text/accent | 5.07Pass on sunken | 9.23Pass on overlay |
| text/success | 6.58Pass on sunken | 9.85Pass on overlay |
| text/warning | 10.43Pass on sunken | 9.51Pass on overlay |
| text/danger | 7.54Pass on sunken | 9.15Pass on overlay |
| text/info | 6.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.
| Token | Light, worst case | Dark, worst case |
|---|---|---|
| border/strong | 3.04Pass on sunken | 4.56Pass on overlay |
| border/focus | 3.34Pass on sunken | 9.23Pass on overlay |
| border/danger | 5.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.
| Token | Light, worst case | Dark, worst case |
|---|---|---|
| interactive/on-accent | 5.96Pass on accent | 7.73Pass on accent |
| Token | Light, worst case | Dark, worst case |
|---|---|---|
| interactive/on-danger | 5.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.
| Token | Light | Dark | Why |
|---|---|---|---|
| text/placeholder | 4.58 | 6.81 | Placeholder 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-pressed | 11.82 | 14.53 | Pressed 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/disabled | 2.04 | 3.03 | WCAG 2.1 exempts inactive components. Low contrast is what communicates that the control cannot be used. |
| text/inert on surface/overlay | 1.72 | 1.97 | The 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
h1per page and an outline underneath it, and put a skip link and amainbefore 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,
alertonly for an error the reader must hear now. - Give a field its
type,inputModeandautoComplete, 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.