Material tokens
The generated reference for all six rungs covers eight properties each, both themes, and the preference overrides that replace them.
Overview
Six rungs, eight properties each, two themes. That is forty-eight declarations in
the base layer plus the preference overrides, and every one of them is generated
from tokens/material.json by scripts/build-tokens.mts. Nothing here is typed
by a person and pnpm check:generated fails the build if the committed output
and a fresh regeneration disagree.
For what the rungs mean, start at The ladder. For which one to use, Choosing a layer. For why the alphas are what they are, The contrast floor. This page is the list.
How it works
The naming grammar
--opsin-material-<rung>-tint backing colour, without its alpha
--opsin-material-<rung>-tint-alpha that colour's alpha, 0..1
--opsin-material-<rung>-blur backdrop-filter blur radius
--opsin-material-<rung>-saturation backdrop-filter saturation multiplier
--opsin-material-<rung>-border hairline colour, or `none`
--opsin-material-<rung>-shadow drop shadow, or `none`
--opsin-material-<rung>-scrim minimum scrim opacity for text to clear the floor
--opsin-material-<rung>-opaque the fallback when translucency is unavailable
<rung> ∈ canvas · card · raised · sheet · overlay · scrimThe name is what appears in CSS, and the ordinal appears nowhere.
--opsin-material-sheet-blur, never --opsin-material-3-blur. The numbers on
The ladder are the ordering and nothing addresses a rung by
one, because a number invites arithmetic such as "one more than a card", and the
ladder is not arithmetic: each rung answers a different question about what is
behind it. The rule is stated in tokens/material.json itself, which is where
the emitter reads it.
tint and tint-alpha are two properties rather than one colour because the
alpha is the safety parameter: it is the number the contrast floor is calculated
from, and it has to be readable on its own by a component and by
scripts/check-contrast.mts.
There is no ninth property and no per-rung variant. A rung is those eight declarations applied together.
What the generator emits
- A
:rootblock with all six rungs for the light theme. - A
.darkblock overriding the rungs that differ. In dark, the translucent rungs' tints are near-black rather than near-white, and the borders flip from dark-at-low-alpha to white-at-low-alpha, because a dark border on a dark surface is invisible. - A
@media (prefers-reduced-transparency: reduce)block collapsing every rung to its opaque fallback (Reduced transparency). All six are emitted, not only the three that are translucent: forcanvas,cardandraisedthe declarations repeat the values above and the block is a no-op, and what that buys is a component being able to readvar(--opsin-material-<rung>-blur)for any rung and be right, with no conditional and no table of which rungs are translucent this month.
The print stylesheet removes backdrop-filter and box-shadow outright rather
than overriding the tokens, because a printed page has no backdrop to filter.
Rungs 0, 1 and 2 reference other tokens
The opaque rungs do not carry literal colours. canvas, card and raised all
resolve their tint to a step of the neutral ramp. In light the step is
--opsin-neutral-0, and in dark it is 950, 900 and 800 respectively, so
the ladder lifts one step at a time where a flat colour would leave the three
indistinguishable. Their borders resolve the same way: --opsin-neutral-200 in
light, 800 and 700 in dark. canvas is the exception to the exception and
has no border at all. Its border token resolves to none, literally, because
the page itself has no edge.
This is deliberate: an opaque material is a composition of the neutral palette, not a parallel palette, and a theme change should move the ladder with it. The translucent rungs still resolve a neutral step for their tint, and what they add is the alpha the neutral palette does not have, which is authored on the rung.
Using it
Apply all eight properties together. A rung's blur without its backing tint is the exact failure the contrast floor exists to prevent, and a rung's tint without its border is a surface with no edge under reduced transparency. Surface applies them as a unit and ships, so reach for it before reaching for the custom properties; if you are writing the CSS yourself, the unit is your responsibility.
Reference the token, never the resolved value. A copied oklch(… / 0.82) is
a translucent surface outside the ladder: it will not respond to reduced
transparency, it will not respond to the theme, and it is in no measured pair.
Do not build a rung out of Tailwind's opacity utilities.
bg-white/70 backdrop-blur-xl looks like a material and is not one, for the same
three reasons. If a utility class is genuinely more convenient, expose the token
as one rather than approximating it.
Do not add a per-component variant. "The sheet on this one screen needs a slightly stronger blur" is how a six-rung ladder becomes a fourteen-value parameter space. If a screen genuinely needs something the ladder does not have, that is intake, not CSS. See 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
Each rung is measured with body text on it, in both themes. An opaque rung is measured on its own colour, and a translucent rung against its worst-case composite. The published results:
| 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 |
Three properties of that table matter and are easy to take for granted:
- A translucent rung is measured as a composite, not as its opaque fallback.
check-contrast.mtscomposites the rung's tint at its per-theme alpha over the darkest and the lightest backdrops the product can produce,--opsin-neutral-950and--opsin-neutral-0, and measures the ink against each, so the published figures forsheetandoverlayare the real worst case rather than the reduced-transparency colour. The method is The contrast floor. The opaque rungs are still measured on their own colour, because that is the only surface they ever show. scrimhas no row, and that is correct. Rung 5 exists to remove the page from consideration, so nothing sits on it and there is no text pair to measure. The generator skips it by name rather than by accident.- Borders are not measured. A hairline at low alpha that disappears over a particular backdrop is an SC 1.4.11 failure, and it is the one most often missed because it looks correct in the single mock everybody reviewed. Nothing on this site has measured one. That is an open gap, and it is named here rather than left to be inferred from a table that does not contain it.
Under reduced transparency a translucent rung collapses to its opaque fallback,
which for sheet and overlay is the card colour, so in that mode their contrast
is card's row. The report above measures the translucent case instead, the
surface these rungs actually render, which is why sheet and overlay now carry
their own composite rows rather than repeating card. scrim is the one rung
whose fallback is its own value, because a scrim that became opaque would be a
blank screen. That value is a dark wash at a higher alpha.
Related
- The ladder says what each rung is for.
- The contrast floor is where the alphas come from.
- Generated token reference has the same data for the whole system, not just materials.
Performance budget
At most three composited surfaces on screen at once, why a blurred backdrop costs what it costs, and how to measure it on the phone your readers actually own.
Motion
Motion for a product somebody opens when worried. What it may do, the four health rules it must never break, and when the right amount of animation is none.