For developers
Semantics, names, roles, state and focus order are split between what the components will do for you and what your JSX still has to get right.
What we guarantee
opsinjs components are built on Base UI primitives wherever they behave like a
dialog, a menu or a listbox: twenty-three of the sixty implementations import
@base-ui/react, so the roles, the keyboard interaction model and the state
attributes for those patterns come from a library that already implements the
APG pattern rather than from us re-deriving it. The rest either compose one of
those twenty-three or present no APG pattern of their own. That choice is the single
largest accessibility decision in the system and it is recorded as
ADR 0001.
On top of that, three contracts are required of every component in the roster:
- State is exposed as data attributes, not as class names.
data-status,data-category,data-open,data-starting-styleanddata-ending-styleare part of the public API and are covered by semver, so you can style and test against them. The shared vocabulary is in Data attributes. - Nothing is announced without being asked. No component mounts a live region on your behalf. Announcement is opt-in with an explicit politeness, because a design system that guesses politeness produces either silence or a screen reader that will not stop talking. The contract is on Screen readers.
- Focus is never trapped except in a modal surface, and a modal surface always returns focus to the element that opened it.
None of this has been verified on a rendered page. Every catalogue row is implemented and installable. No component has been reviewed, and the catalogue records no review date for any of the sixty. So these remain the requirements an implementation has to satisfy, not descriptions of tested behaviour. Their having been written is not evidence that they meet them.
What you own
The accessible name of everything you render. This is the most common real
failure, and it is not something a component can fix. An icon-only button gets
its name from you. A RangeBar renders a bar; the sentence that says what is
in range is yours.
DOM order. CSS order is not reading order. order, grid-area, row-reverse
and absolute positioning all decouple the two, and screen readers, sequential
navigation and the browser's own find-in-page all follow the DOM. If you cannot
make the DOM order correct, the layout is wrong.
Labels and their programmatic association. Placeholder text is not a label. A visually hidden label is a label. A label that only appears once the field has focus is not.
Input purpose. SC 1.3.5 wants autocomplete on inputs that collect
information about the user, and health forms collect a lot of it. Name, date of
birth, postcode, telephone and email all have defined tokens. See
Ask users for… for the per-question
guidance.
Error handling that survives a screen reader. An error rendered in red beside a field, with no programmatic association and no announcement, does not exist for a large fraction of your readers. The pattern is on Error and empty messages for the words and Validation timing for the mechanics.
How to check
Unplug the mouse
Do the whole task with the keyboard only. Every interactive element must be reachable, the focus indicator must be visible at every stop, and you must never land somewhere you cannot get out of. Nothing else on this list finds as many bugs per minute. The expected behaviour is specified on Keyboard and focus.
Read the accessibility tree, not the DOM
Open the accessibility tree in your browser's developer tools and walk the screen. You are looking for elements with a role and no name, elements with a name that duplicates the visible text badly ("button button"), and headings that skip levels. The tree is what assistive technology sees; the DOM is not.
Query the way a test would
In a test, find elements by role and accessible name. The query is
getByRole('button', { name: 'Log a reading' }). If you cannot express the
element that way, a screen-reader user cannot find it either. This turns an
accessibility property into an ordinary failing test, which is the only form of
accessibility check that survives a deadline.
Force the preference queries on
Toggle prefers-reduced-motion, prefers-reduced-transparency and
prefers-contrast in developer tools and re-run the task. In this codebase the
first two are implemented in app/globals.css and app/product.css; the third
is not, and that gap is documented on
Increased contrast. Emulating a preference is not the
same as testing it, but it catches the class of bug where a transition was the
only signal that something changed.
Turn the network off mid-task
Health UI reads stale data as if it were live more often than it reads nothing at all. Check that the stale state is distinguishable programmatically and not only by a greyer colour. See Uncertainty, staleness and missing data.
Measured results
The registry holds sixty built components, but no generator reads an
accessibility result out of them: scripts/build-registry.mts has no
conformance step, <A11yReport> takes its counts as props, and no catalogue row
records a review. There is code to aggregate and no measurement to
aggregate from.
Nothing on this site types a measured number by hand, so an ungenerated table shows this rather than an example.
Per-component conformance blocks, keyboard tables and the aggregated keyboard reference are all meant to be generated from the registry. Until a person or a tool that reads a rendered accessibility tree has actually evaluated a component, its row here would be a number nobody produced, and the placeholder above is the honest substitute. What does run today is described under Known gaps: static source assertions, and nothing else.
Known gaps
- The assertions that do run are static ones.
pnpm run check:a11yblocks every pull request, but it reads your component as text: it sees that a file imports a lucide icon and readsCLINICAL_STATUS_META, not that either one reaches the DOM, and a status word hidden in ansr-onlyspan passes it clean. There is still no axe integration and no jsdom test, so nothing anywhere asserts a property of a rendered accessibility tree. - The announcement contract is specified but unimplemented, so there is no shared helper for polite and assertive regions yet.
- No RTL testing has been done. The system is authored logical-property-first but that has not been verified. See Internationalisation and RTL.
Updates to this page
Last read through against the system on 2026-09-20. Due for review every 6 months; expiry is reported by pnpm run check:freshness.