Accessibility

Accessibilitymatters.

1.66:1 · fails 4.52:1 · passes hover to fix it

Design is not the part that looks good. Whether text can be read is a measurement rather than an opinion — and one with a number, a standard, and in more than forty countries a law behind it.

The laws differ. The standard does not. The Americans with Disabilities Act, the European Accessibility Act, the UK Equality Act, the Accessible Canada Act, Australia’s Disability Discrimination Act — written differently, enforced differently, argued differently, and nearly all of them pointing at the same technical document. WCAG, Level AA. Which is the useful part: a product sold everywhere needs one answer, not one per market.

So a colour is not picked because somebody likes it. The second line above is warning yellow on the primary fill — 1.66 to 1, and both of them ours. That is exactly why someone would reach for it. Hovering swaps it for the value the system already carries for text on that fill.

01 — What is true

What the system already does

None of this is aspiration. Each one is in a component spec or in the reset, and most of it is enforced by the components rather than left to whoever is building a screen.

The ring is keyboard-only:focus-visible, never :focus. Two pixels of the primary, offset by two. Clicking a control focuses it without showing the halo, which is why the ring means something when you do see it.
The browser does the keyboard workSelect is a native <select>, so tab, type-ahead, arrows, Escape and focus-return are the platform’s. Radio groups share a name, so arrow navigation is free. The rule in the specs is blunt: do not add a JS keyboard handler, it will fight the browser.
Roving tabindex where it is neededIn a tablist one trigger holds tabindex=0 and the rest hold −1, so Tab moves into the group and then out of it rather than through every tab. The handler lives on the list, not on each trigger, so they cannot fight each other.
Roles, and the names that go with themtablist / tab / tabpanel with aria-controls and aria-labelledby; menu / menuitem; aria-haspopup and aria-expanded on a submenu trigger. Ids are generated per instance, so two groups on one page never collide on a reference.
Switch is a switchrole="switch" is deliberate: a screen reader says on and off rather than checked and unchecked. And the component is not to be wrapped — putting a <button> around a native input costs both the accessibility and the form participation.
Reduced motion is answered, not ignoredThe reset collapses every transition, and the JavaScript scale returns zero under the same query so Web Animations match. Where a movement carried meaning — the drawer’s blocked-dismiss nudge — reduced motion gets a static outline instead. An alternative, not an absence.
Contrast is measured, not judgedEvery text value against every surface it is allowed to sit on, in both themes: 208 checks, all clearing 4.5:1. Warning is the one documented exception and runs dark text on its solid at 5.06:1, because a dark yellow is a brown.
02 — The rule underneath

Use the platform before writing anything

The single most useful decision in this system is that the form controls are native elements. A <select> arrives already knowing type-ahead, arrow keys, Escape, focus return, and how to announce itself to a screen reader. None of that had to be built, and none of it can rot.

Almost every accessibility bug is something re-implemented worse. A <div> with a click handler has no role, no name, no keyboard behaviour and no focus — and each of those is then added back, separately, by someone who has to remember all four.

The specs put it as a prohibition rather than a preference, which is the right strength: do not add a JS keyboard handler.

03 — The ring

Only when it helps

A focus ring on every click is noise, and noise gets designed away — which is how systems end up with no ring at all. :focus-visible is the browser deciding for you: it shows the ring when the focus came from a keyboard and hides it when it came from a pointer.

from the keyboard2px, offset 2px
from a clickfocused, no ring

The left one is drawn as it would appear; the right one is a real button you can click. Tab to either and the browser will show you the real thing.

04 — What is missing

What is missing

This is the part of the page that matters. Everything above is already true; everything below is not, and the first one is worse than absent — it is a contradiction the system is carrying in two places at once.

The focus ring is two coloursComponents draw 2px solid var(--color-primary-solid-900). The token called focus-state is rgba(0, 120, 255, 1) — blue, from before the primary was sienna. Two answers to one question, and the wrong one has the better name.
No minimum target sizeNothing states one. 44 by 44 is the usual floor and most controls clear it by accident rather than on purpose, which means the next small one will not.
No skip linkEvery page puts the nav before the content, and there is no way past it from the keyboard except through it.
Nothing for forced coloursWindows high-contrast mode replaces the palette wholesale. A system this dependent on its own colours should say what happens when they are taken away.
No convention for announcing changeToasts, async results, validation. Nothing says what is polite, what is assertive, or which one a failed save should be.
.sr-only is defined where it is usedIt exists in the drawer. It should exist once.
No stated way of checkingNo keyboard pass, no automated sweep, no screen-reader run named as part of shipping. Everything above is true because people were careful, which is not the same as it staying true.