skip to content
menu
foundations · accessibility

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 label is 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-invalid and aria-busy, and aria-disabled on menu items.
  • Data table. A grid, or a treegrid when grouped, with row headers, levels, positions, and aria-rowcount for rows not rendered yet.
  • Live regions. A banner's text alone is the region: role=status (polite), role=alert for 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-motion where they implement it.
  • Your part, in code. An aria-label on each table, a rowLabel for the checkboxes and row menus, one h1 per page (the worklist's headingLevel).
  • Known gaps, in code. Text fields, selects and textareas draw focus with a border colour and an inset shadow, which forced colours remove.