The ladder
The six named rungs are canvas, card, raised, sheet, overlay and scrim, each with its job, its eight properties and its opaque fallback.
Overview
Six rungs, ordered by how much separation the surface claims from what is behind it. The first three are opaque and the last three are translucent, and that boundary between rung 2 and rung 3 is the most important line in the system, because everything above it is subject to the contrast floor and everything below it is not.
0Canvas
What is the page itself?
The application background. Nothing is behind it, so nothing shows through it.
1Card
Is this a distinct piece of content on the page?
A grouped block of related values, such as a result or a day's readings. Opaque, bordered, no shadow. The workhorse.
2Raised
Is this temporarily above the page, but not covering it?
A card that has been lifted: a selected row, a dragged tile, a result being acted on.
3Sheet
Is this covering the page while leaving it recognisable underneath?
A bottom sheet or side panel the reader can dismiss. The blur keeps enough of the page visible that they know where they will return to.
4Overlay
Is this chrome floating over scrolling content?
A sticky header, a filter bar, a popover. The rung most often misused for content that has to stay readable.
5Scrim
Am I trying to make everything behind this unusable on purpose?
The dimming layer behind a modal. Its job is to remove the page, not to be looked at.
The backdrop is generated in CSS, not photographed, so it is the same worst case on every machine. Measured contrast for these pairs is published by pnpm run contrast on foundations/materials/the-contrast-floor. This specimen shows you the problem, and that page gives you the numbers.
The demonstration stacks all six over a deliberately hostile photographic backdrop, with the scrim and reduced-transparency toggles live, so that the floor is something you can see rather than something this page asserts.
How it works
The six rungs
| # | Name | Opaque? | Blur | Its job |
|---|---|---|---|---|
| 0 | canvas | yes | None | the page itself; nothing is behind it, so nothing shows through it |
| 1 | card | yes | None | a distinct piece of content on the page, bounded by a line rather than a shadow; the default home for a health value |
| 2 | raised | yes | None | temporarily above the page but not covering it: a menu, a popover, a tooltip, a dragged card; the highest opaque rung |
| 3 | sheet | no | large | something covering the page while leaving it recognisable underneath: a bottom sheet, a side panel, a modal |
| 4 | overlay | no | largest | chrome floating over scrolling content: a pinned toolbar, a tab bar, a floating action bar |
| 5 | scrim | no | minimal | the dimming layer behind a sheet, whose job is to remove the page from consideration |
Read rows 3 and 4 twice. overlay here is the chrome that content scrolls
beneath; the surface that covers the page is sheet. Two retired vocabularies
used those two words the other way round, so a positional translation puts a
dismissible sheet on the chrome rung and a tab bar on the sheet rung, and both
compile and render.
ADR 0014 settles which
set wins and why: translate by job, never by ordinal.
The names are the vocabulary. Use them in design review, in commit messages, and
in the rung prop on Surface. The custom
properties carry the same names, so a sheet's blur is
--opsin-material-sheet-blur and never --opsin-material-3-blur. A number
invites arithmetic and the ladder is not arithmetic. The ordinals in the first
column are the ordering and nothing addresses a rung by one.
The properties, rung by rung
Each rung resolves the same eight custom properties:
--opsin-material-<rung>-tint, -tint-alpha, -blur, -saturation,
-border, -shadow, -scrim and -opaque. Materials is where
each of them is defined. The shape of the
ladder is easier to see as a set of movements than as a table of values:
- canvas → card adds a boundary. No shadow, and in dark the backing lifts one
step off the page colour. A
cardis a region of content, not an object. - card → raised adds the only ordinary shadow in the system. Rung 2 is where a surface becomes an object that is temporarily above the page.
- raised → sheet is the boundary. The backing gains an alpha, a blur appears, the saturation is lifted above 1 to compensate for the desaturation the blur causes, and the border becomes a hairline expressed in alpha rather than a solid neutral. The shadow shrinks almost to nothing, because a sheet's separation comes from the blur.
- sheet → overlay lowers the alpha, raises the blur and the saturation further, and brings back a large soft shadow. It is the thinnest material in the system and therefore the one whose contrast floor is hardest to hold.
- overlay → scrim is a different kind of thing entirely:
scrimis a dark wash with almost no blur, whose job is to remove information rather than to present any.
Exact resolved values are in the generated table below, in both themes. They are not repeated in prose anywhere on this site, because a value in prose is a value that will be wrong after the next regeneration.
Blur, tint, saturation and vibrancy
Four things are commonly bundled into "frosted glass". opsinjs uses three of them, each for a stated reason, and deliberately declines the fourth.
Tint is used. The backing colour and its alpha. This is the safety parameter: it is what bounds the composite and therefore what makes contrast measurable. See The contrast floor.
Blur is used, for a narrow reason. backdrop-filter: blur() removes high
spatial frequency from what is behind the surface. It takes the texture out of
a photograph so that glyph edges are not competing with edges behind them. It
does not meaningfully change the local mean luminance, so it does not fix
contrast, and it is never traded against tint.
Saturation boost is used, on two rungs only. Boosting the saturation of the
backdrop is what gives platform materials their characteristic liveliness, and
here it does a narrower job: a large blur desaturates whatever is behind it, so
without a boost sheet and overlay look grey rather than translucent. It is a
per-rung token and the other four rungs sit at 1, the identity, so a boost is
something a rung declares rather than something the ladder does everywhere.
Vibrancy is not used. Platform vibrancy blends the foreground text with the backdrop as well, which is a beautiful effect and an unbounded contrast liability. Text on an opsinjs material is opaque, always.
Bound the composite for the two saturated rungs. The published figures for a
material are measured against its opaque fallback, which saturation never
touches, so the boost on sheet and overlay is outside every number this
site prints. The open question is whether a saturated backdrop can still be
bounded by the tint and its alpha alone, or whether a boosted rung needs a
higher minimum tint to compensate.
Browser support, honestly
backdrop-filter: blur() is safe to use everywhere a modern browser runs, and
has been for years. Two caveats that are still real:
- Older Safari needs the
-webkit-prefix, which costs one line and is emitted. backdrop-filterwith an SVG filter reference is a Chromium-only capability, with the syntaxbackdrop-filter: url(#something). Nothing in this ladder uses it, and nothing in this ladder ever will, because a rung that only exists in one engine is not a rung.
Where backdrop-filter is unavailable entirely, rungs 3, 4 and 5 land on their
opaque fallbacks, which are the same fallbacks that reduced transparency and
print use. That path is exercised by real readers, not just by a feature query,
so it is designed rather than tolerated.
| Feature | Your browser | What happens without it |
|---|---|---|
| backdrop-filter | Checking… | Translucent material rungs fall back to their opaque colour. Layout is unchanged and the contrast floor is unchanged, because the fallback is the value the floor was measured against. |
| corner-shape (squircle) | Checking… | A plain rounded rectangle at the same radius. A taste difference, never a functional one. |
| Display-P3 colour | Checking… | The sRGB ramp. The escalation only raises chroma and keeps hue and lightness identical. Every measured contrast figure therefore holds in both gamuts. |
| linear() easing | Checking… | The browser's default easing. A spring is one custom property holding one linear() value, so there is no cubic-bezier approximation behind it: the declaration that substitutes it is invalid at computed-value time and the timing function is lost. Identical duration and identical end state; the curve is what goes. |
| :has() | Checking… | The status matrix filter and a handful of docs-only affordances stop filtering and show every row. Nothing in the product theme depends on it. |
Tested live with CSS.supports() in the browser you are reading this in. Nothing here is a claim about a browser matrix somebody maintained by hand.
Using it
Say the rung's name. A design review conversation that contains "make it a
bit more glassy" produces a screen with four different alphas. One that contains
"that should be overlay, not sheet" produces a screen with two.
The boundary between rung 2 and rung 3 is a decision, not a gradient. Ask
what is behind the surface and whether the reader still needs it: a surface that
covers the page while leaving it recognisable is sheet, chrome that content
passes beneath is overlay, and anything else that needs to look lifted is
raised, opaque, with a shadow. There is no half-translucent card.
A toast is raised, not overlay. A toast appears over arbitrary content it
knows nothing about, frequently over a photograph or a chart, and it usually
carries a message that matters. It must not depend on what is behind it, so it
is opaque.
A scrim never appears alone. A scrim exists to isolate a sheet. A dimmed
screen with nothing on top of it is a loading state pretending to be a modal.
Do
A results list on canvas, each reading as a card, a menu opened from a
row as raised, a sticky summary bar as overlay, and a detail panel as a
sheet over a scrim. Six rungs on one screen and never more than three
composited at any moment, because only the last three are translucent.
Don’t
Every card given the overlay material because the frosted look reads as
premium. Each card is now a compositing layer, each one's contrast depends on
the row of the photograph behind it, and the sticky chrome has lost the only
visual property that distinguished it from the content.
Do not add a rung. If a surface does not fit one of the six, the usual cause is that it is trying to be two things: a card that is also chrome, a sheet that is also a page. Splitting it is almost always the right fix; the intake process for a genuine seventh rung is in Contributing tokens.
Tokens
| Token | What it controls | Used by |
|---|---|---|
| --opsin-material-canvas-tintvar(--opsin-neutral-0) | Rung 0 (canvas): the tint over what is behind it. The application background. Nothing is behind it, so nothing shows through it. | surface |
| --opsin-material-canvas-tint-alpha1 | Rung 0 (canvas): how opaque that tint is. | surface |
| --opsin-material-canvas-blur0px | Rung 0 (canvas): backdrop blur radius. | surface |
| --opsin-material-canvas-saturation1 | Rung 0 (canvas): backdrop saturation multiplier. | surface |
| --opsin-material-canvas-bordernone | Rung 0 (canvas): the boundary. | surface |
| --opsin-material-canvas-shadownone | Rung 0 (canvas): the shadow that separates it from what is behind. | surface |
| --opsin-material-canvas-scrim0 | Rung 0 (canvas): the minimum scrim opacity needed for text on this rung to clear the contrast floor. | surface |
| --opsin-material-canvas-opaquevar(--opsin-neutral-0) | Rung 0 (canvas): the opaque substitute used under prefers-reduced-transparency and where backdrop-filter is unsupported. | surface |
| --opsin-material-inset-tintvar(--opsin-neutral-100) | Rung 0.5 (inset): the tint over what is behind it. The recessed well inside a card: the explanatory note a Callout draws inside the reading it annotates, and the track a RangeBar lays its range along. Its fill sits below the card fill in both themes, so the well reads as carved into the card rather than lifted off it. In light it is neutral-100, one step under the white card, which is where a Callout already sat. In dark it is a value between the page and the card, below the card so the well is a recess and above the page so a Callout used on its own is still visible. | callout, range-bar, surface |
| --opsin-material-inset-tint-alpha1 | Rung 0.5 (inset): how opaque that tint is. | surface |
| --opsin-material-inset-blur0px | Rung 0.5 (inset): backdrop blur radius. | surface |
| --opsin-material-inset-saturation1 | Rung 0.5 (inset): backdrop saturation multiplier. | surface |
| --opsin-material-inset-bordernone | Rung 0.5 (inset): the boundary. | surface |
| --opsin-material-inset-shadownone | Rung 0.5 (inset): the shadow that separates it from what is behind. | surface |
| --opsin-material-inset-scrim0 | Rung 0.5 (inset): the minimum scrim opacity needed for text on this rung to clear the contrast floor. | surface |
| --opsin-material-inset-opaquevar(--opsin-neutral-100) | Rung 0.5 (inset): the opaque substitute used under prefers-reduced-transparency and where backdrop-filter is unsupported. | surface |
| --opsin-material-card-tintvar(--opsin-neutral-0) | Rung 1 (card): the tint over what is behind it. The default home for a health value. Opaque, bounded by a line rather than a shadow, and the rung every ResultCard, MetricTile and RangeBar sits on. | surface |
| --opsin-material-card-tint-alpha1 | Rung 1 (card): how opaque that tint is. | surface |
| --opsin-material-card-blur0px | Rung 1 (card): backdrop blur radius. | surface |
| --opsin-material-card-saturation1 | Rung 1 (card): backdrop saturation multiplier. | surface |
| --opsin-material-card-bordervar(--opsin-neutral-200) | Rung 1 (card): the boundary. | surface |
| --opsin-material-card-shadownone | Rung 1 (card): the shadow that separates it from what is behind. | surface |
| --opsin-material-card-scrim0 | Rung 1 (card): the minimum scrim opacity needed for text on this rung to clear the contrast floor. | surface |
| --opsin-material-card-opaquevar(--opsin-neutral-0) | Rung 1 (card): the opaque substitute used under prefers-reduced-transparency and where backdrop-filter is unsupported. | surface |
| --opsin-material-raised-tintvar(--opsin-neutral-0) | Rung 2 (raised): the tint over what is behind it. Menus, popovers, tooltips, a dragged card. Still opaque: the shadow, not translucency, is what says 'above'. | surface |
| --opsin-material-raised-tint-alpha1 | Rung 2 (raised): how opaque that tint is. | surface |
| --opsin-material-raised-blur0px | Rung 2 (raised): backdrop blur radius. | surface |
| --opsin-material-raised-saturation1 | Rung 2 (raised): backdrop saturation multiplier. | surface |
| --opsin-material-raised-bordervar(--opsin-neutral-200) | Rung 2 (raised): the boundary. | surface |
| --opsin-material-raised-shadow0 1px 2px oklch(0 0 0 / 0.06), 0 4px 12px oklch(0 0 0 / 0.06) | Rung 2 (raised): the shadow that separates it from what is behind. | surface |
| --opsin-material-raised-scrim0 | Rung 2 (raised): the minimum scrim opacity needed for text on this rung to clear the contrast floor. | surface |
| --opsin-material-raised-opaquevar(--opsin-neutral-0) | Rung 2 (raised): the opaque substitute used under prefers-reduced-transparency and where backdrop-filter is unsupported. | surface |
| --opsin-material-sheet-tintvar(--opsin-neutral-0) | Rung 3 (sheet): the tint over what is behind it. A bottom sheet or side panel the reader can dismiss by dragging. Its tint is now near opaque, so the panel lands close to the card colour and a white control inside it no longer floats above a greyer surface. What keeps the covered page recognisable is the scrim around the panel rather than the page reading through the panel, because at this opacity very little of the page shows through the tint. | surface |
| --opsin-material-sheet-tint-alpha0.93 | Rung 3 (sheet): how opaque that tint is. | surface |
| --opsin-material-sheet-blur20px | Rung 3 (sheet): backdrop blur radius. | surface |
| --opsin-material-sheet-saturation1.6 | Rung 3 (sheet): backdrop saturation multiplier. | surface |
| --opsin-material-sheet-borderoklch(0 0 0 / 0.08) | Rung 3 (sheet): the boundary. | surface |
| --opsin-material-sheet-shadow0 -1px 2px oklch(0 0 0 / 0.05) | Rung 3 (sheet): the shadow that separates it from what is behind. | sheet, surface |
| --opsin-material-sheet-scrim0.93 | Rung 3 (sheet): the minimum scrim opacity needed for text on this rung to clear the contrast floor. | surface |
| --opsin-material-sheet-opaquevar(--opsin-neutral-0) | Rung 3 (sheet): the opaque substitute used under prefers-reduced-transparency and where backdrop-filter is unsupported. | surface |
| --opsin-material-overlay-tintvar(--opsin-neutral-0) | Rung 4 (overlay): the tint over what is behind it. A pinned toolbar, a tab bar and a floating action bar are all chrome that content scrolls beneath. | surface |
| --opsin-material-overlay-tint-alpha0.74 | Rung 4 (overlay): how opaque that tint is. | surface |
| --opsin-material-overlay-blur28px | Rung 4 (overlay): backdrop blur radius. | surface |
| --opsin-material-overlay-saturation1.8 | Rung 4 (overlay): backdrop saturation multiplier. | surface |
| --opsin-material-overlay-borderoklch(0 0 0 / 0.10) | Rung 4 (overlay): the boundary. | surface |
| --opsin-material-overlay-shadow0 8px 32px oklch(0 0 0 / 0.12) | Rung 4 (overlay): the shadow that separates it from what is behind. | combobox, menu, popover, select, surface, toast, tooltip |
| --opsin-material-overlay-scrim0.74 | Rung 4 (overlay): the minimum scrim opacity needed for text on this rung to clear the contrast floor. | surface |
| --opsin-material-overlay-opaquevar(--opsin-neutral-0) | Rung 4 (overlay): the opaque substitute used under prefers-reduced-transparency and where backdrop-filter is unsupported. | surface |
| --opsin-material-scrim-tintvar(--opsin-neutral-950) | Rung 5 (scrim): the tint over what is behind it. The dimming layer behind a modal dialog or a consent sheet. Its job is to remove the page from consideration, not to look like glass. | surface |
| --opsin-material-scrim-tint-alpha0.44 | Rung 5 (scrim): how opaque that tint is. | surface |
| --opsin-material-scrim-blur2px | Rung 5 (scrim): backdrop blur radius. | surface |
| --opsin-material-scrim-saturation1 | Rung 5 (scrim): backdrop saturation multiplier. | surface |
| --opsin-material-scrim-bordernone | Rung 5 (scrim): the boundary. | surface |
| --opsin-material-scrim-shadownone | Rung 5 (scrim): the shadow that separates it from what is behind. | surface |
| --opsin-material-scrim-scrim0.44 | Rung 5 (scrim): the minimum scrim opacity needed for text on this rung to clear the contrast floor. | surface |
| --opsin-material-scrim-opaqueoklch(0.205 0.006 250 / 0.72) | Rung 5 (scrim): the opaque substitute used under prefers-reduced-transparency and where backdrop-filter is unsupported. | surface |
Accessibility impact
The opaque rungs (0, 1, 2) carry the ordinary obligation. Their backgrounds are known colours, so text on them is measured exactly as any other pair is. See Contrast and APCA.
The translucent rungs (3, 4, 5) carry a stronger one. Their rendered colour depends on the backdrop, so they are measured against the worst-case composite rather than against their nominal tint: the rung's tint at its own alpha over a pure-black and a pure-white backdrop, in both themes. Where a rung clears the floor at both extremes it is safe over imagery and video; where it does not the report below records the shortfall rather than hiding it. The calculation is The contrast floor.
Borders are not measured yet. A hairline at low alpha over a translucent rung is the most commonly missed non-text contrast failure (SC 1.4.11), because it looks correct on the one backdrop the designer tested, and nothing on this site has measured one. It is an open gap, named here rather than implied by a table that does not hold the row. Material tokens states the same.
The fallback path is a first-class path. Reduced transparency is not a degraded experience. The rung keeps its border, its shadow and its position, and only its translucency changes, so nothing moves and nothing is lost. Reduced transparency states it rung by rung.
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the canvas material | light | 104.7 | 17.96:1 | Pass |
| body text on the canvas material | dark | -100.5 | 18.00:1 | Pass |
| body text on the inset material | light | 98.1 | 16.32:1 | Pass |
| body text on the inset material | dark | -100.1 | 17.18:1 | Pass |
| body text on the card material | light | 104.7 | 17.96:1 | Pass |
| body text on the card material | dark | -99.6 | 16.32:1 | Pass |
| body text on the raised material | light | 104.7 | 17.96:1 | Pass |
| body text on the raised material | dark | -97.5 | 13.77:1 | Pass |
| body text on the sheet material over the darkest backdrop | light | 99.9 | 16.76:1 | Pass |
| body text on the sheet material over the darkest backdrop | dark | -99.7 | 16.47:1 | Pass |
| body text on the sheet material over the lightest backdrop | light | 104.7 | 17.96:1 | Pass |
| body text on the sheet material over the lightest backdrop | dark | -80.8 | 6.07:1 | Pass |
| body text on the overlay material over the darkest backdrop | light | 85.9 | 13.48:1 | Pass |
| body text on the overlay material over the darkest backdrop | dark | -99.9 | 16.77:1 | Pass |
| body text on the overlay material over the lightest backdrop | light | 104.7 | 17.96:1 | Pass |
| body text on the overlay material over the lightest backdrop | dark | -54.2 | 2.69:1 | Below floor |
Related
- Choosing a layer has the decision table from surface kind to rung, plus the nesting rules.
- The contrast floor explains why rungs 3, 4 and 5 have the alphas they have.
- Layering and elevation has the z-index contract, which the ladder correlates with but does not define.
Materials
Six ordered rungs answer what a surface is made of and what is behind it. Each rung has a stated job, an opaque fallback and a contrast floor it has to clear.
Choosing a layer
A decision table from surface kind to rung, five nesting rules, and the three questions that resolve almost every case.