opsinjs

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-style and data-ending-style are 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 aggregated conformance table has not been generated.

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:a11y blocks every pull request, but it reads your component as text: it sees that a file imports a lucide icon and reads CLINICAL_STATUS_META, not that either one reaches the DOM, and a status word hidden in an sr-only span 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.

On this page