Screen readers
The combinations we intend to test, the announcement contract every status change must satisfy, and an honest table of untested cells.
What we guarantee
The announcement contract
This is the part of screen-reader support that a design system can genuinely own, and it is the part most systems leave undefined. Four rules:
- Nothing is announced unless a component was asked to announce it. No opsinjs component mounts a live region on your behalf. A library that guesses produces either silence where it mattered or a screen reader that will not stop talking, and the second is worse: readers turn the feature off.
- A value change is
polite; a safety change isassertive. A metric that updates because a background sync completed must not interrupt. An alert that escalates tourgentwhile the reader is on the screen must. There is no third politeness and no automatic escalation. - The announcement is a sentence, not a fragment. It says "Blood pressure, 138 over 84, above your usual range" rather than "138". The fragment is what you get when the live region wraps only the number, which is the default mistake.
- The status word is in the accessible name, not only in the colour. This is the same rule as Colour independence seen from the other side: a screen reader is a permanently greyscale device.
The intended tested matrix
| Screen reader | Platform | Browser | Status |
|---|---|---|---|
| VoiceOver | iOS | Safari | Not tested |
| VoiceOver | macOS | Safari | Not tested |
| NVDA | Windows | Firefox | Not tested |
| NVDA | Windows | Chrome | Not tested |
| JAWS | Windows | Chrome | Not tested |
| TalkBack | Android | Chrome | Not tested |
| Narrator | Windows | Edge | Not planned |
Mobile combinations are listed first deliberately. A consumer health app is read on a phone, so VoiceOver on iOS and TalkBack on Android are the primary targets and desktop is secondary. That ordering is the reverse of the priority most design systems use.
Every cell says Not tested, and it will keep saying so until somebody has actually done it. No component exists to test. This table is a plan, published so that its emptiness is visible rather than absent.
What you own
Every accessible name. Components supply structure and state; the words come from you. An icon-only button is unlabelled until you label it.
Politeness in context. We can specify that a change is announced politely. Only your product knows whether five tiles updating at once should produce five announcements (they should not) or one summary.
Reading order. A screen reader reads the DOM. See For developers.
Spoken numbers. "138/84 mmHg" is not read the way you say it. Decide what the spoken form should be and supply it; the formatting rules are on Grammar and mechanics.
Tables and charts. A chart needs a text equivalent that answers the same question. See Alt text and descriptions and Accessible charts.
How to check
Configure the screen reader properly, once
Learn the four gestures or keystrokes you need and nothing more. They are next item, previous item, next heading and activate. Testing with a screen reader you have not configured produces confident wrong conclusions, which is worse than not testing.
Read the whole screen linearly
Move item by item from the top. Expected result: the sequence tells a coherent story and every value is accompanied by what it is and what it means. Failure looks like a run of bare numbers, or an image with no alternative text, or a heading announced as a paragraph.
Trigger every state change and listen
Submit the form. Let the value refresh. Escalate the alert. Expected result: the change is announced once, at the right politeness, as a sentence. Failure looks like silence after a successful save, or the same announcement three times because three regions updated.
Navigate by heading and by landmark
Screen-reader users navigate structurally far more than they read linearly. If the heading list is not a usable table of contents, the screen has no structure regardless of how it looks.
Test with the screen off
For a mobile screen reader, turn the screen curtain on. It removes the temptation to fill gaps with what you can see.
Measured results
scripts/build-registry.mts. Run pnpm run generate and reload.Nothing on this site types a measured number by hand, so an ungenerated table shows this rather than an example.
No screen-reader testing has been performed. When it is, results will be recorded
per component as an <A11yReport> block naming the assistive technology, its
version, the browser, the date and the outcome, because an untimestamped
screen-reader result is not reproducible.
Known gaps
- Every cell in the matrix is untested.
- There is no shipped announcement helper, so the politeness contract above is specified and unimplemented.
- Braille output is not considered anywhere in this system, and numeric formatting for a braille display is a real and unaddressed question.
- Voice control (Dragon, Voice Control, Voice Access) is a distinct interaction mode that depends on visible labels matching accessible names, and we have written no guidance for it.
Updates to this page
Last read through against the system on 2026-09-02. Due for review every 6 months; expiry is reported by pnpm run check:freshness.
Keyboard and focus
The global keyboard contract every opsinjs component inherits, and the focus rules that a health screen breaks most often.
Reduced motion
Reduced motion is a per-token degradation, not a kill switch. This page says what every animated token collapses to, and why the state change survives.