opsinjs
FoundationsToken familiesMaterials

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.

Backdrop
  • 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

#NameOpaque?BlurIts job
0canvasyesNonethe page itself; nothing is behind it, so nothing shows through it
1cardyesNonea distinct piece of content on the page, bounded by a line rather than a shadow; the default home for a health value
2raisedyesNonetemporarily above the page but not covering it: a menu, a popover, a tooltip, a dragged card; the highest opaque rung
3sheetnolargesomething covering the page while leaving it recognisable underneath: a bottom sheet, a side panel, a modal
4overlaynolargestchrome floating over scrolling content: a pinned toolbar, a tab bar, a floating action bar
5scrimnominimalthe 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 card is 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: scrim is 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.

Not written yet.

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-filter with an SVG filter reference is a Chromium-only capability, with the syntax backdrop-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.

FeatureYour browserWhat happens without it
backdrop-filterChecking…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 colourChecking…The sRGB ramp. The escalation only raises chroma and keeps hue and lightness identical. Every measured contrast figure therefore holds in both gamuts.
linear() easingChecking…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

TokenWhat it controlsUsed 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-alpha1Rung 0 (canvas): how opaque that tint is.surface
--opsin-material-canvas-blur0pxRung 0 (canvas): backdrop blur radius.surface
--opsin-material-canvas-saturation1Rung 0 (canvas): backdrop saturation multiplier.surface
--opsin-material-canvas-bordernoneRung 0 (canvas): the boundary.surface
--opsin-material-canvas-shadownoneRung 0 (canvas): the shadow that separates it from what is behind.surface
--opsin-material-canvas-scrim0Rung 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-alpha1Rung 0.5 (inset): how opaque that tint is.surface
--opsin-material-inset-blur0pxRung 0.5 (inset): backdrop blur radius.surface
--opsin-material-inset-saturation1Rung 0.5 (inset): backdrop saturation multiplier.surface
--opsin-material-inset-bordernoneRung 0.5 (inset): the boundary.surface
--opsin-material-inset-shadownoneRung 0.5 (inset): the shadow that separates it from what is behind.surface
--opsin-material-inset-scrim0Rung 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-alpha1Rung 1 (card): how opaque that tint is.surface
--opsin-material-card-blur0pxRung 1 (card): backdrop blur radius.surface
--opsin-material-card-saturation1Rung 1 (card): backdrop saturation multiplier.surface
--opsin-material-card-bordervar(--opsin-neutral-200)Rung 1 (card): the boundary.surface
--opsin-material-card-shadownoneRung 1 (card): the shadow that separates it from what is behind.surface
--opsin-material-card-scrim0Rung 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-alpha1Rung 2 (raised): how opaque that tint is.surface
--opsin-material-raised-blur0pxRung 2 (raised): backdrop blur radius.surface
--opsin-material-raised-saturation1Rung 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-scrim0Rung 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.93Rung 3 (sheet): how opaque that tint is.surface
--opsin-material-sheet-blur20pxRung 3 (sheet): backdrop blur radius.surface
--opsin-material-sheet-saturation1.6Rung 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.93Rung 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.74Rung 4 (overlay): how opaque that tint is.surface
--opsin-material-overlay-blur28pxRung 4 (overlay): backdrop blur radius.surface
--opsin-material-overlay-saturation1.8Rung 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.74Rung 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.44Rung 5 (scrim): how opaque that tint is.surface
--opsin-material-scrim-blur2pxRung 5 (scrim): backdrop blur radius.surface
--opsin-material-scrim-saturation1Rung 5 (scrim): backdrop saturation multiplier.surface
--opsin-material-scrim-bordernoneRung 5 (scrim): the boundary.surface
--opsin-material-scrim-shadownoneRung 5 (scrim): the shadow that separates it from what is behind.surface
--opsin-material-scrim-scrim0.44Rung 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.

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

On this page