accessibility
Our screens are for everyone: people who use a mouse, people who only use a keyboard, people who listen to the screen with a screen reader, and people who zoom in or turn animation off. Accessibility is part of the baseline every screen meets, not a feature for later.
The standard we meet is WCAG 2.2 at level AA. Most of the work happens inside the components: each one knows what it is, what it is called, where the keyboard goes and what to announce. So the obvious way to use a component is also the accessible way. Where a component can't do something for you yet, its page says so.
keyboard and focus
Everything can be reached and used without a mouse. The keyboard focus is always visible, and when something closes, focus goes back to where it came from.
Controls that belong together, such as a row of tabs or the rows of a table, are one tab stop: tab moves into the group, and the arrow keys move inside it. The whole model, with menus, dialogs and the focus outline, is on interaction.
names and states
Every control has a name that a screen reader announces, its accessible name. Usually it is the label people see. That also helps people who use speech input: they say the words they see, and the control answers to them.
- A control that is only an icon has no visible words, so it needs a name written for it. The icon button and the action menu ask for one. Name the thing it acts on, not only the action: “actions for S04E07”, not “more”.
- A button or a menu with words is named by those words.
- States are told, not only drawn. When a tab is selected, a section is open, a column is sorted, a field has an error or something is busy, the component also tells assistive technology, so a screen reader can say it.
- The data table tells a screen reader how it is built: which cell names each row, how the groups nest, and how many rows there are in all, including the ones not shown yet.
announcements
Some changes happen away from where people are looking. A screen reader announces them through a live region, so nobody has to go and look for them.
| what changes | what people hear |
|---|---|
| A banner appears or changes | Its text. An error is read at once; other messages wait until the screen reader is free. A note that never changes isn't announced. |
| A toast appears | Its text, once the screen reader is free. The toasts' area is on the page before the first toast, so the first one isn't missed. |
| The selection changes | The selection toolbar says “8 selected”, or “selection cleared”. |
| A filter changes the list | The filter bar's count, such as “312 of 2,500”. |
| A table shows more rows | The new count, such as “200 of 2,500”. |
| A status is a warning or an error | Its state first, then its words: “warning: connection firewalled”. |
colour and motion
Colour never carries a meaning on its own. Every state also has a symbol of its own shape and words, and a selected row also has a ticked checkbox. People who can't tell the colours apart, and people who can't see them at all, get the same message.
Motion is short, and only there to say something. The components that move stop moving when people turn on reduced motion, where they support it; motion lists exactly what each one does.
how it is checked
Automatic tests check what can be checked automatically, every time the code changes:
- Contrast. Every text colour is tested on every background and every state's tint, and has to reach a contrast ratio of at least 4.5 to 1. The edges of controls, the focus outline and ticked checkboxes need at least 3 to 1. A colour change that falls short fails the build. Colour shows every pair.
- Structure. An accessibility checker runs over every component, in each state its tests show: what each part is, what it is called, and how the page is built.
- In a browser. The same checker runs over every example on the React subsite, where it can check the colours on screen too.
- Behaviour. Tests press keys and check where the focus goes and what is announced.
Passing these tests is the floor, not proof. Nothing here tests with a real screen reader automatically, so trying a new screen with one is still worth the time.
known gaps
These are the gaps that matter most across the system. Each component's page lists its own, under “Today:”.
- No light, high-contrast or forced-colours look yet. With forced colours on, text fields, selects and text areas, which show focus with a coloured border and a soft inner shadow, can look as if nothing has focus. These looks are planned, together with the themes.
- The tooltip appears on hover and when its own wrapper takes focus, adds an extra tab stop, and can't be closed with esc (WCAG 1.4.13). Its page has the details.
- The modal: when several are open, one press of esc closes them all, and the page behind can still be reached.
- No radio group and no searchable list yet. Use the browser's own radio buttons, and the patterns on the input pages.
- The fonts aren't included in the package, so text shows in a fallback font until the app loads them.
your part
The components can't know your content, so a few things are up to you:
- Give every table a name, and give each row's checkbox the row's name.
- Keep one main heading per page. A worklist inside an app takes a lower heading level when the shell owns the main one.
- Write messages that make sense when they are read aloud on their own, without their colour or their icon.
- Keep raw ids and codes out of the main text; put them behind a “details” link.
specs
| check | where | what it covers |
|---|---|---|
| Token contrast | tokens/contrast.test.ts, in npm test and CI |
Every text token on every background and state tint at 4.5:1; control boundaries, the focus ring and checked fills at 3:1 |
| Axe in unit tests | src/**/*.test.tsx, in jsdom |
Every component in each state its tests render: roles, names, ARIA use, structure. jsdom can't compute colour, so contrast is the token test's job there |
| Axe in a browser | The React subsite's accessibility panel, and a run in a headless browser over every story | The rendered stories, colour contrast included |
| Behaviour | The unit tests, with Testing Library | Keyboard maps, focus movement and live-region text |
- Names. The icon button's and the action menu's
labelis required. A visible label is the accessible name (WCAG 2.5.3, label in name). - States. The components set
aria-selected,aria-expanded,aria-sort,aria-invalidandaria-busy, andaria-disabledon menu items. - Data table. A
grid, or atreegridwhen grouped, with row headers, levels, positions, andaria-rowcountfor rows not rendered yet. - Live regions. A banner's text alone is the region:
role=status(polite),role=alertfor errors,live="off"for a static note. The toaster's list is a polite region that exists before the first toast. The selection toolbar's count, the filter bar's count (always present) and the data table's growing count are polite. A status puts a visually hidden “warning: ” or “error: ” before its text. - Motion. Components that animate honour
prefers-reduced-motionwhere they implement it. - Your part, in code. An
aria-labelon each table, arowLabelfor the checkboxes and row menus, oneh1per page (the worklist'sheadingLevel). - Known gaps, in code. Text fields, selects and textareas draw focus with a border colour and an inset shadow, which forced colours remove.