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-visibleyourself, 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
rolereaches the DOM asdata-roleonly. Alignment and colour carry it to sighted readers and to nobody else, so add a visually hidden prefix or anaria-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, androle="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
toolbarof disclosures, because atablistmay hold only tabs and must always have one selected. - The composer's editor omits
aria-expanded, because ARIA forbids it ontextboxandrole="combobox"would forbidaria-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.