opsinjs

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

This table has not been generated. It is produced by 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, and scripts/check-a11y.mts --layout loads them through ?text=200. The check nevertheless needs Playwright, which is deliberately in no package.json, so it runs only in the nightly workflow and nothing on this site reports what a run found. The mechanism is verifiable in app/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.

On this page