Design system

Accessibility

Accessibility is part of each component rather than a layer applied afterwards. This page states what the library guarantees, what it cannot decide for you, and how those guarantees are verified.

What every component guarantees

  • A visible focus indicator. One ring treatment, driven by :focus-visible so it appears for keyboard users without following a mouse click around. It is never removed — outline: none with no replacement is the single most common accessibility regression in component libraries.
  • Semantic HTML underneath. Buttons are <button>, links are <a>, headings are real heading elements. ARIA is used to describe behaviour HTML cannot express, not to re-implement what it already does.
  • Keyboard operability. Anything interactive is reachable and operable by keyboard alone, with the interaction pattern its role implies.
  • Contrast at WCAG AA. Every default token pairing that renders text clears 4.5:1, or 3:1 for large text. Borders that convey state clear 3:1 against their surroundings.
  • Reduced motion is honoured. Transitions collapse under prefers-reduced-motion. Where motion carries meaning, it degrades to a static state instead of disappearing.
  • Decorative content is hidden. Icons that duplicate adjacent text are aria-hidden, so nothing is announced twice.

Where the API makes you do the right thing

The most reliable accessibility mechanism is a type error. Several components make the accessible choice the only one that compiles, or the only one that is convenient.

// An icon-only button has no text to announce, so the
// accessible name is required rather than optional. TypeScript
// enforces it — omitting aria-label is a build error.
<IconButton aria-label="Delete item" icon={<TrashIcon />} />
// Colour is not available to a screen reader. Where the tone
// carries the meaning, supply the words it stands for.
<Badge tone="danger" srLabel="Status:">Overdue</Badge>
// FormField generates the id, links the label, joins the
// description and error into aria-describedby in reading order,
// and sets aria-invalid when there is an error.
<FormField label="Email" description="Used to sign you in." error={error}>
  <Input type="email" />
</FormField>

Live regions, chosen by meaning

Alert derives its ARIA role from its tone. danger and warning use role="alert" with aria-live="assertive", interrupting immediately; info and success use role="status" with aria-live="polite", waiting for a pause. Getting this backwards either buries a genuine error or hijacks the user mid-sentence to tell them something trivial.

What the library cannot do for you

  • Heading order. Heading separates level from size, so you can render an h3 that looks large without lying about the document outline — but only you know the outline.
  • Meaningful names. “Click here” passes every automated check and helps nobody.
  • Reading and focus order. Both follow your DOM order. A layout that visually reorders content with CSS will read in the original order.
  • Contrast after you retheme. Overriding tokens discards the tuning the defaults received. See Theming.
  • Whether an interaction is appropriate at all. A perfectly implemented modal is still the wrong answer to many problems.

Behaviour borrowed deliberately

Focus trapping, focus restoration, roving tabindex, type-ahead and collision-aware positioning are delegated to Radix primitives. These are the patterns where hand-rolled implementations reliably ship subtle bugs — focus escaping a dialog behind the browser chrome, arrow keys landing on a disabled item, a menu opening off-screen. Radix is treated as an invisible behaviour layer: none of its API surfaces in ABBA's props, so it can be replaced without a breaking change for you.

How it is tested

All 27 components have unit tests covering keyboard interaction and ARIA relationships, plus an automated axe pass. Both run in CI, so an accessibility regression fails the build rather than reaching a release.

// Every component suite runs an axe pass.
it("has no axe violations", async () => {
  const { container } = render(<Alert tone="danger" title="Failed" />);
  await expect(container).toHaveNoAxeViolations();
});

The color-contrast rule is disabled in those runs. jsdom does no layout or paint, so the rule cannot evaluate and would report a false pass — contrast is verified against the token values instead, where it can actually be measured.

Automated testing is not enough

Automated tooling catches a minority of real accessibility problems. It cannot tell you whether a label is meaningful, whether the focus order makes sense, or whether an announcement arrives at a useful moment. Test with a keyboard, and test with a screen reader.

Reporting a problem

Accessibility bugs are treated as correctness bugs, not enhancements. Open an issue with the component, the assistive technology and browser, and what you expected to happen.