opsinjs
AccessibilityContracts

Reduced transparency

What each rung of the material ladder becomes when the reader has asked for less translucency. The layout must not move, and this page says why.

What we guarantee

Three of the six rungs on the material ladder are translucent, and translucency over arbitrary content is the single most reliable way to produce text that cannot be read. prefers-reduced-transparency: reduce is therefore implemented as a first-class degradation in app/globals.css and app/product.css, not as an afterthought.

RungNormallyUnder reduced transparency
0Opaque page background, no blurUnchanged
1Opaque card surface, no blurUnchanged
2Opaque card surface with shadowUnchanged
3Translucent, 8 px backdrop blurOpaque card surface, blur removed
4Translucent, 20 px backdrop blurOpaque card surface, blur removed
5Translucent veil, slight blurOpaquer veil, blur removed

The rungs are numbered here rather than named; their names, their intended uses and the rule for choosing between them are on The ladder. The values above are the ones actually declared for --opsin-material-N-bg and --opsin-material-N-blur in the stylesheet.

Two guarantees follow from that table:

Nothing moves. Only the background colour and the blur radius change. No padding, no radius, no size, no position. A layout that is correct with translucency is byte-for-byte the same layout without it, which means the preference cannot introduce a reflow bug and cannot be a source of a truncated value.

The topmost rung gets more opaque, not less. Rung 5 is the veil that separates a modal surface from what is behind it. Removing its translucency entirely would remove the separation, so instead it darkens. The reader asked for less see-through, not for less contrast.

The full ladder, the rungs' intended uses and the contrast floor that governs text on them are on The ladder and Reduced transparency in Foundations.

What you own

Any translucent surface you build yourself. A custom overlay with rgba() and backdrop-filter is not reached by the media query above. Either build it from a rung, or add your own reduced-transparency branch.

Photographic and video backdrops. The ladder's measured contrast assumes the declared opaque fallback behind the surface. Text over a photograph has no guaranteed contrast at all, and the fix is a scrim, not a heavier font weight. See The contrast floor.

Your composited-surface budget. Reduced transparency is also a performance lever: with blur removed, the compositing cost of a screen drops. That does not license three stacked blurred surfaces the rest of the time. The budget is at most three, and the reasoning is on Performance budget.

Windows and Linux. prefers-reduced-transparency maps cleanly to the Apple and Android settings. On Windows the closest control is the transparency-effects toggle, and browser support for reporting it has historically been uneven. Do not assume the query fires everywhere; treat it as an enhancement that is available rather than guaranteed.

How to check

Turn the preference on

macOS and iOS: Settings → Accessibility → Display → Reduce transparency. Android: Settings → Accessibility → Colour and motion. Or emulate the query in developer tools for a quick pass.

Screenshot before and after and difference them

Expected result: the two images differ only in the fill and blur of rungs 3, 4 and 5. Failure looks like anything moving. A shifted baseline, a changed card height and a repositioned close button all mean that something other than colour was tied to the translucent style.

Read the text on every translucent surface over your worst backdrop

Put the busiest, highest-contrast content you have behind the surface: a photograph, a dense chart, a wall of text. Then check the reading with transparency both on and off. The off case is the easy one.

Check the modal separation

Open a dialog with the preference on. Expected result: the content behind is clearly subordinate. Failure looks like a scrim so light that the reader cannot tell which layer is interactive, which is a keyboard and screen-reader problem as much as a visual one.

Measured results

PairThemeAPCA LcWCAG 2.2Floor
body text on the canvas materiallight104.717.96:1Pass
body text on the canvas materialdark-100.518.00:1Pass
body text on the inset materiallight98.116.32:1Pass
body text on the inset materialdark-100.117.18:1Pass
body text on the card materiallight104.717.96:1Pass
body text on the card materialdark-99.616.32:1Pass
body text on the raised materiallight104.717.96:1Pass
body text on the raised materialdark-97.513.77:1Pass
body text on the sheet material over the darkest backdroplight99.916.76:1Pass
body text on the sheet material over the darkest backdropdark-99.716.47:1Pass
body text on the sheet material over the lightest backdroplight104.717.96:1Pass
body text on the sheet material over the lightest backdropdark-80.86.07:1Pass
body text on the overlay material over the darkest backdroplight85.913.48:1Pass
body text on the overlay material over the darkest backdropdark-99.916.77:1Pass
body text on the overlay material over the lightest backdroplight104.717.96:1Pass
body text on the overlay material over the lightest backdropdark-54.22.69:1Below floor

Contrast on rungs 3 and 4 is measured against each rung's declared opaque fallback, which is the reduced-transparency value. That makes the published figure the reduced-transparency figure, and the normal case is measured against the same fallback rather than against a real backdrop. That is an optimistic simplification, and it is stated here rather than hidden.

Known gaps

  • No real-backdrop measurement exists. We measure against the fallback, not against a distribution of photographs.
  • Windows and Linux support for the query is not verified in this codebase, and there is no JavaScript fallback.
  • One component renders the ladder. Surface reads --opsin-material-<rung>-* for all six rungs and swaps to the opaque fallback under the query; every other built component sits on a rung rather than choosing one, so the ladder has had exactly one implementation to be wrong in.

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