opsinjs
AccessibilityContracts

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:

  1. 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.
  2. A value change is polite; a safety change is assertive. A metric that updates because a background sync completed must not interrupt. An alert that escalates to urgent while the reader is on the screen must. There is no third politeness and no automatic escalation.
  3. 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.
  4. 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 readerPlatformBrowserStatus
VoiceOveriOSSafariNot tested
VoiceOvermacOSSafariNot tested
NVDAWindowsFirefoxNot tested
NVDAWindowsChromeNot tested
JAWSWindowsChromeNot tested
TalkBackAndroidChromeNot tested
NarratorWindowsEdgeNot 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.

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

This table has not been generated. It is produced by 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.

On this page