---
title: Accessibility
description: What the primitives guarantee, and what your application still has to supply.
---

The primitives own roles, relationships, keyboard behaviour and focus movement.
They render no styles, so everything visual is yours — and some of it is not
optional.

## What you have to supply

- **Focus indication.** Focus moves correctly; nothing paints it. Style
  `:focus-visible` yourself, and remember that in command lists and ask-user
  options the highlight *is* the indicator, so a faint highlight is a missing
  focus ring rather than a soft hover.
- **Contrast.** No colour ships with the package.
- **Speaker identity.** A message's `role` reaches the DOM as `data-role` only.
  Alignment and colour carry it to sighted readers and to nobody else, so add a
  visually hidden prefix or an `aria-label`.
- **Status copy.** Validation emits a code, not a message, and nothing announces
  "generating" beyond a button label. Add your own `role="status"` region where
  in-flight state matters, and `role="alert"` for failures.
- **Anything you build in a seam.** A surface lifted through a render prop
  inherits that pattern's obligations — a hover preview still has to open on
  focus and close on Escape.

## Keyboard

The primitives follow the [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/).

A composite widget is one tab stop, not one per item: the arrow keys move within
it and Tab moves past it. Focus follows the arrows without selecting, and a
primitive that should select as focus moves says so through a prop. Anything
draggable is operable from the keyboard too.

Which keys a primitive claims is on its own page, in a Keyboard table. A
primitive with no table claims none and relies on native focus order.

## Names

Interactive parts ship default accessible names and containers do not, because
containers are not controls. Every default is overridable through `aria-label`
or `aria-labelledby`.

Where a name would otherwise repeat down a list, the part takes the
distinguishing piece as a prop rather than guessing. `Attachments.Remove` takes
a `filename`, so five buttons in a row do not all announce "Remove attachment".

Anything announced as content rather than as a name is text you render, so it is
localised wherever the rest of your copy lives.

## Deviations

Twice, the standard role cannot describe the widget, and the primitive says so
rather than failing quietly. Both are argued on the page that makes the choice:

- [Tabs](/primitives/tabs#why-a-toolbar-and-not-a-tablist) is a `toolbar` of
  disclosures, because a `tablist` may hold only tabs and must always have one
  selected.
- The [composer](/primitives/composer)'s editor omits `aria-expanded`, because
  ARIA forbids it on `textbox` and `role="combobox"` would forbid
  `aria-multiline`.

## Testing

The package runs an axe-based suite over the composer combobox wiring, the
ask-user flows, the display primitives' status attributes, and accessible names
across controls. Automated checks cannot evaluate contrast or focus visibility
in your styling, so those need your own pass.
