Accessibility

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.

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 is a toolbar of disclosures, because a tablist may hold only tabs and must always have one selected.
  • The 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.