---
title: "Accessibility"
description: "What opsinjs guarantees, what your product still owns, and how to check either one without taking our word for it."
url: "https://opsinjs.pensievelabs.org/accessibility"
source: "https://opsinjs.pensievelabs.org/accessibility.md"
section: "Accessibility"
kind: "accessibility"
reviewed: "2026-09-20"
reviewer: "design"
aliases: ["a11y", "wcag 2.2", "accessibility overview"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="accessibility" />

## What we guarantee [#what-we-guarantee]

A design system cannot make a product accessible. It can make the accessible
version the cheap one, and it can refuse to hand you the parts that make an
inaccessible screen easy to build. That is the guarantee here, and it is
deliberately narrower than "opsinjs is accessible".

Four commitments hold across everything in this system, and each one is checkable
rather than asserted:

1. **Clinical status is never carried by colour alone.** Every status surface
   emits a word and a `data-status` attribute alongside its colour. Reading the
   page in greyscale, or through a screen reader, or on a photocopy, must give
   the same answer as reading it in colour. This is WCAG 2.2 SC 1.4.1 Use of
   Colour, but the reason we treat it as a safety rule rather than a conformance
   rule is on [Colour independence](./colour-independence.mdx).
2. **The touch-target floor is 44 CSS pixels, not 24.** WCAG 2.2 SC 2.5.8 sets
   24×24 as the AA minimum. We ship the AAA figure from SC 2.5.5 as the floor,
   for reasons that are specific to health apps and are set out on
   [Target size and motor accessibility](./target-size-and-motor.mdx).
3. **Every animated token carries a reduced-motion value.** Reduced motion is a
   per-token degradation, not a global kill switch, so a component still changes
   state. It simply arrives immediately. See
   [Reduced motion](./reduced-motion.mdx).
4. **Every accessibility number published on this site is generated, dated and
   reproducible.** Contrast figures come out of `scripts/check-contrast.mts` and
   are committed; they are not typed by an author. Where the generator has not
   run, the page says so instead of showing a plausible figure.

<Callout>
  Every catalogue row is implemented and installable. That changes what these
  commitments cover, though less than it sounds. They cover the token layer, the
  CSS in `app/globals.css` and `app/product.css`, and the page contracts every
  component specification must satisfy. For all sixty, they also cover
  whatever `pnpm run check:a11y` can prove by reading a component source. Every one
  of the sixty has now been audited against WCAG 2.2 AA (author-run), in a source
  pass and a rendered pass, and the findings were fixed in that same change. That
  audit was run by the authors of the components, so it is not an independent
  review, and no clinician has reviewed any of the sixty. Read
  [ADR 0025](../project/decisions/0025-the-audit-is-author-run.mdx) for what the
  audit was and what it was not.
</Callout>

## What you own [#what-you-own]

The boundary matters more than the promise. These are yours, and none of them can
be moved into a design system without the system pretending to know things about
your product that it cannot know.

* **Accessible names.** A `Button` gives you focus handling and a target size. It
  cannot know that "Continue" needs to be "Continue to your results" for someone
  arriving at it out of context.
* **Reading and focus order.** The DOM order is written by you. A visually
  correct two-column layout with a scrambled tab order passes every automated
  check we could ship.
* **Thresholds and clinical meaning.** opsinjs will render "attention" in the
  attention colours, with its own glyph shape and the word "Needs attention".
  Deciding that a systolic reading of a particular value *is* "attention" is a
  clinical decision owned by your product and whoever signs off its clinical
  safety case. See
  [Safety, scope and limitations](../start/safety-scope-and-limitations.mdx).
* **Announcement politeness.** We can specify that a status change must be
  announced. Only you know whether the change happened because the reader
  submitted something (announce it) or because a background sync completed
  (usually do not interrupt them).
* **Timing, sessions and authentication.** SC 2.2.1, 3.3.8 and 3.3.9 are product
  decisions. A design system that shipped a session timeout would be shipping a
  clinical risk.
* **Your content.** Everything under [Content and language](../content/index.mdx)
  is guidance we can hold you to at review; it is not something the components
  can enforce.

## How to check [#how-to-check]

Start from the role you are actually in. The five role pages are the same
standard seen from five desks, and each one is a list of things you can do today
rather than a restatement of WCAG.

* [For designers](./for-designers.mdx) covers contrast, target size, focus
  appearance and colour independence, checked before handoff.
* [For developers](./for-developers.mdx) covers semantics, names, roles, state
  and focus order, checked in the browser.
* [For content](./for-content.mdx) covers headings, link text, alt text and
  reading level, checked while writing.
* [For testing](./for-testing.mdx) has the manual and automated passes, and how
  to reproduce each one.
* [For compliance reviewers](./for-compliance-reviewers.mdx) is the curated
  twenty-minute path, written to be printed.

Then use the per-topic pages for the specific question. Each of those carries its
own reproducible check with an expected result and a description of what failure
looks like, because "test with a screen reader" is not a test.

## Measured results [#measured-results]

<SectionProgress />

Contrast is the accessibility property this system measures most confidently,
because it is a property of the tokens rather than of a component: `pnpm run
contrast` computes it without rendering anything. The measured APCA Lc and WCAG
2.2 ratios for every published token pair appear on
[Contrast conformance](./contrast-conformance.mdx), dated, in both themes.

It is no longer the only one. `pnpm run check:a11y` reads every component source
on each pull request and fails on a status carried by colour alone, a `px` type
size, a raw colour literal, both colour axes on one element, a banned word in
copy or in an identifier, and a role that exists only under the documentation
chrome. It reads text, not a rendered page, so it can prove a component does not
do those things and cannot prove that what it renders is usable. The
layout-dependent half is hit area, 1.3× and 200% against the built `/view`
routes. That half runs nightly, on a schedule, which means it goes red the
morning after a merge rather than before it.

Every other measured result on this site is currently absent, and the pages that
would carry those numbers say so in words rather than filling the gap.

## Known gaps [#known-gaps]

These are real, and they are listed here rather than in an internal tracker
because a conformance claim with no gaps section is not a conformance claim.

* **No component has been tested with a screen reader.** All sixty were audited
  against WCAG 2.2 AA by their authors, in a source pass and a rendered pass, but
  that rendered pass never put one of them in front of a screen reader.
  [Screen readers](./screen-readers.mdx) states the intended tested matrix and
  marks every cell untested.
* **`prefers-contrast` is not answered in the token layer.** `app/globals.css`
  handles `prefers-reduced-motion` and `prefers-reduced-transparency` today and
  carries no increased-contrast block. One component has gone ahead of it:
  `Surface` drops its translucency and widens its edge under
  `prefers-contrast: more` in its own utilities, unmeasured, and the colour half
  of the escalation is answered nowhere. See
  [Increased contrast](./increased-contrast.mdx) for what the token layer will
  change and [Surface](../components/surface.mdx) for what already changes.
* **The automated checks read source and layout, never comprehension.** Two
  now run. `scripts/check-a11y.mts` blocks every pull request and reads the
  component sources as text. It can prove a file imports a glyph and reads the
  status vocabulary, and it cannot prove either one reaches the screen. Its
  `--layout` half runs once a night against the built `/view` routes and
  measures three things: hit area, survival at 1.3x, and 200% with no truncated
  value. Neither half is an axe pass, neither has met a screen reader, and
  neither looks at this documentation site.
* **No independent conformance evaluation has been performed.** The author-run
  audit against WCAG 2.2 AA is not an ACR, and the
  [conformance report](./conformance-report.mdx) is still a complete ACR skeleton
  in which every criterion is marked *Not Evaluated*, which is the honest state.
* **Cognitive accessibility guidance is not yet backed by user research.**
  [Cognitive accessibility](./cognitive-accessibility.mdx) is marked as opinion
  throughout.

## Updates to this page [#updates-to-this-page]

<Reviewed />
