---
title: "For designers"
description: "The five checks to run on a health screen before it leaves your hands, and the three decisions only a designer can get wrong."
url: "https://opsinjs.pensievelabs.org/accessibility/for-designers"
source: "https://opsinjs.pensievelabs.org/accessibility/for-designers.md"
section: "Accessibility"
kind: "accessibility"
reviewed: "2026-09-02"
reviewer: "design"
aliases: ["design review", "before handoff", "design checks"]
---

> 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]

The tokens are the guarantee. If you design with the published colour roles, the
material ladder, the spacing scale and the type scale, four properties come for
free and you do not have to re-derive them per screen:

* **Contrast.** Every published status and category token is paired with an ink
  token that has been measured against it. You do not choose a text colour to sit
  on a status surface; the pair is a token pair, and the measurement of that pair
  is published on [Contrast conformance](./contrast-conformance.mdx).
* **Target size.** `--opsin-target-minimum` is `2.75rem` in the product theme.
  That is 44px at the default root font size, and it grows with the type when a
  reader enlarges it. The density setting deliberately does not touch it either.
  Compact density moves spacing only. A "compact" screen is never a harder
  screen to hit.
* **Chroma is gamut-aware, lightness is not.** The `@supports (color-gamut: p3)`
  block in the stylesheet raises chroma on wide-gamut displays and leaves
  lightness alone, so the measured contrast of every pair is identical on an sRGB
  laptop and a P3 phone. A screen that reads correctly on your display reads
  correctly on the reader's.
* **The two axes never mix.** Category colour says *what this measurement is
  about*; status colour says *how urgent it is*. The system will not give you a
  token that is both, and the reasoning is on
  [The two colour axes](../health/two-colour-axes.mdx).

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

Three things go wrong at design time and cannot be repaired downstream.

**Focus appearance.** The system supplies a focus ring token. Whether the ring is
visible on *your* composition is a layout question: a ring drawn inside a card
that is clipped by `overflow: hidden`, or hidden behind a sticky bottom bar, is a
failure of SC 2.4.7 Focus Visible and, in the sticky-bar case, of SC 2.4.11 Focus
Not Obscured. Draw the focused state of every interactive element in your
composition, including the one at the bottom of a long scroll.

**Reading order.** You own the order the eye takes and, by implication, the order
the DOM should take. When a two-column layout puts a summary to the right of a
chart, say explicitly which one comes first in the source. If you do not, the
developer will guess, and the guess is usually "left to right".

**What each colour is allowed to mean.** A designer who introduces a sixth status
colour, or reuses the sleep category purple for a warning, breaks a contract the
whole system rests on. Category palettes may be extended; status palettes may
not. See [Status palettes](../theming/status-palettes.mdx).

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

Five checks, in the order that catches the most for the least effort. Run them on
the composition, not on a screenshot of it.

<Steps>
  ### Turn the colour off [#turn-the-colour-off]

  Desaturate the whole board. Every piece of information that was carried by colour
  must still be readable, and that includes the status word, the icon and the
  position of a marker on a range. If a screen becomes ambiguous in greyscale, the
  fix is never a darker colour; it is a word. Full method on
  [Colour independence](./colour-independence.mdx).

  ### Measure the pairs you invented [#measure-the-pairs-you-invented]

  Any text or icon you placed on a background that is *not* a published token pair
  needs measuring. Use the checker at `/playground/contrast`, and record both
  numbers: APCA Lc for the perceptual answer and the WCAG 2.2 ratio for the
  conformance answer. They disagree in predictable places, which is exactly why we
  publish both. See
  [Contrast and APCA](../foundations/colour/contrast-and-apca.mdx).

  ### Put a 44 px square on every tappable thing [#put-a-44-px-square-on-every-tappable-thing]

  The square goes on the hit area rather than on the visible shape. A 24 px icon
  button with 10 px of padding on every side passes; the same icon in a dense list
  row with 4 px of padding does not. Adjacent targets also need separation, and
  the rule for that is on
  [Target size and motor accessibility](./target-size-and-motor.mdx).

  ### Set the text to 200% [#set-the-text-to-200]

  Change the text size and not the zoom of the artboard, because a text-size change
  is what a reader with Dynamic Type turned up actually gets. Every layout in the
  system must reflow rather than truncate, and a number that is truncated is a
  clinical failure, not a cosmetic one. The mechanism and the expected result are
  on [Text resizing and zoom](./text-resizing-and-zoom.mdx).

  ### Draw the four data states [#draw-the-four-data-states]

  Empty, loading, error and stale. Most accessibility failures in health UI happen
  in states nobody drew: a stale reading rendered identically to a fresh one, an
  error state with no route out of it, a loading skeleton that never announces it
  has finished. See
  [Data states](../foundations/data-states.mdx) for the definitions and
  [Error and empty messages](../content/error-and-empty-messages.mdx) for what to
  write in them.
</Steps>

## Measured results [#measured-results]

The design-time numbers you are entitled to rely on are the published token-pair
measurements, and they exist. `scripts/check-contrast.mts` has run in this
checkout, `lib/generated/contrast.json` is committed, and `contrast:verify`
fails the build if a token edit moves a figure. The numbers on
[Contrast conformance](./contrast-conformance.mdx) and in the
[generated contrast reference](../reference/generated/contrast.mdx) are therefore
the ones the shipped tokens actually produce, in both themes, dated by the run.

What is not measured is anything you compose. A pair you invent, text over a
photograph, two chart series against each other, and the effective background of
a translucent rung over your content are all outside that file. Measure those
yourself rather than assuming the published figure covers them.

## Known gaps [#known-gaps]

* **There is no Figma library.** Token export to W3C DTCG JSON and a Figma
  Variables mapping are specified on
  [Design handoff](../theming/design-handoff.mdx) but not yet built, so today the
  tokens have to be read out of the stylesheet.
* **There is no annotation kit.** Designers currently have no shipped way to
  record reading order, alternative text or announcement intent on a
  composition; those have to be written in prose alongside it.
* **No component redlines exist.** Every implemented component now publishes its
  parts as a labelled list under *Anatomy* on its own page, but a redline is a
  drawing that gives the measured spacing, size and offset a designer can copy
  off it, and none has been produced. Read the parts list and the token values
  instead, and expect to derive the geometry yourself.

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

<Reviewed />
