Text resizing and zoom
The 200% text check and the 400% reflow check are different tests that fail in different ways. It also says why a truncated number is a safety defect.
What we guarantee
Two separate things, which are routinely confused:
Text resizing (SC 1.4.4). Text can be scaled to 200% without loss of content
or functionality. In this system that is real: the product theme has no fixed
pixel type sizes at the root, sets -webkit-text-size-adjust: 100% so mobile
browsers do not silently rescale, and the preview harness exposes it as a genuine
root font-size change. app/product.css does that with
[data-text-size="200"] { font-size: 200% } rather than a transform: scale().
That distinction matters: a transform makes everything bigger and proves nothing,
whereas a root font-size change makes text bigger inside a layout that did not
grow, which is the condition that actually breaks things.
Reflow (SC 1.4.10). Content reflows to a 320 CSS pixel equivalent width
without two-dimensional scrolling. In practice this is tested as 400% browser
zoom at a 1280 px viewport. The layout system is built for it: the spacing scale
derives from a single --spacing custom property, containers are
percentage-and-ch based rather than fixed, and the responsive-mode rules on
Responsive modes treat narrow as a
first-class mode rather than a degradation.
And one health-specific guarantee: a numeric value must never truncate. A
component may wrap, stack, abbreviate its label or drop its decoration, but the
number and its unit are the payload. A RangeBar that clips "138" to "13" at
200% text is not a layout bug; it is a screen that displays a false reading. Every
component specification in the roster carries this as an accessibility
requirement.
What you own
Fixed heights. The single most common cause of a 200% failure is a container with a height in pixels. At 200% the text inside it is twice as tall and either overflows or is clipped. Use minimum heights and let content set the rest.
overflow: hidden used as a tidiness tool. It converts an overflow bug into
an invisible data-loss bug, which is worse. If content must be clipped, it must
also be reachable another way.
Single-line assumptions. Buttons, chips, table headers and status pills are usually designed at one line. At 200%, in German, with a long clinical term, they are not. Decide now whether they wrap or truncate. For anything carrying a value, the answer is wrap.
Viewport meta. user-scalable=no and maximum-scale=1 disable pinch zoom and
are an outright failure. The docs site does not set them; check that your product
does not either.
Text spacing (SC 1.4.12). Readers apply user stylesheets that increase line height, letter spacing and word spacing. Layouts that assume a fixed line height break under this and it is not covered by the two checks above.
How to check
The 200% text check
In the preview harness, switch text size to 200% with the text-size control on
any <ComponentPreview>, which stamps data-text-size on the embedded /view
document. In a product, use the operating system: iOS Settings → Accessibility
→ Display and Text Size → Larger Text at maximum, or the Android font-size
slider, or a browser minimum-font-size setting.
Expected result: everything reflows, nothing is clipped, no number is truncated, and every interactive element is still reachable. Failure looks like a value with an ellipsis in it, a button whose label has vanished, or a sticky footer that has eaten the bottom third of the screen.
The 400% zoom check
Set the browser window to 1280 px wide and zoom to 400%. Expected result: no horizontal scrollbar for the page as a whole; content is in a single column; tables scroll horizontally within their own container rather than making the page scroll. Failure looks like a two-dimensional scroll, where the reader has to move right to read the end of a sentence and then left again for the next one.
The both-at-once check
Do them together. This is not required by either criterion and it is where health dashboards actually fall over, because a card grid that survives each test separately can still produce a 4 px-wide column when both apply.
The Dynamic Type check on a real device
Simulated font scaling in a browser is not the same as a platform text-size setting, which also changes system control metrics. Test on hardware if the product ships to phones. The type system's side of this is on Dynamic Type.
Measured results
scripts/capture-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.
The intended generated artefact is a captured render of every component at 100%
and 200% text in both themes. The check underneath it exists and runs:
scripts/check-a11y.mts --layout loads every built /view route at 390x844 at
1x, again with the root font size at 1.3x, and again through the route's own
?text=200, and it fails when an element's scrollWidth exceeds its
clientWidth. Any [data-opsinjs-value] that truncates gets a separate, louder
failure, because a missing digit is the failure this page is about. The check
runs in .github/workflows/nightly.yml, the one job that installs Playwright;
Playwright is still in no package.json. What is missing is the captured render:
the check reports what failed and keeps no picture of what passed.
Known gaps
- No 200% result is published. The check exists and has real routes to point
at. Every implemented component renders at
/view, andscripts/check-a11y.mts --layoutloads them through?text=200. The check nevertheless needs Playwright, which is deliberately in nopackage.json, so it runs only in the nightly workflow and nothing on this site reports what a run found. The mechanism is verifiable inapp/product.css; the result is not. - Text spacing (SC 1.4.12) is not addressed anywhere in this system. Line height is a token; the behaviour of the layout when a user stylesheet overrides it is untested and unspecified.
- No long-string or pseudo-localisation fixtures exist, so the wrapping behaviour of labels is unproven.
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.