---
title: "The ladder"
description: "The six named rungs are canvas, card, raised, sheet, overlay and scrim, each with its job, its eight properties and its opaque fallback."
url: "https://opsinjs.pensievelabs.org/foundations/materials/the-ladder"
source: "https://opsinjs.pensievelabs.org/foundations/materials/the-ladder.md"
section: "Foundations"
kind: "foundation"
reviewed: "2026-09-02"
reviewer: "design"
aliases: ["material rungs", "surface ladder", "canvas card raised sheet overlay scrim", "elevation levels"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="foundation" />

## Overview [#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](./the-contrast-floor.mdx) and everything below it is not.

<MaterialLadder />

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 [#how-it-works]

### The six rungs [#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](../../project/decisions/0014-material-rung-names.mdx) 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](../../components/surface.mdx). 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 [#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](./index.mdx) 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 [#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](./the-contrast-floor.mdx).

**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.

<Todo>
  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.
</Todo>

### Browser support, honestly [#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.

<BrowserSupport feature="backdrop-filter" />

## Using it [#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.

<DoDont>
  <DoDont.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.
  </DoDont.Do>

  <DoDont.Dont>
    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.
  </DoDont.Dont>
</DoDont>

**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](../../handbook/contributing/contributing-tokens.mdx).

## Tokens [#tokens]

<TokenTable scope="material" />

## Accessibility impact [#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](../colour/contrast-and-apca.mdx).

**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](./the-contrast-floor.mdx).

**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](./tokens.mdx) 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](./reduced-transparency.mdx) states it rung by rung.

<ContrastReport scope="materials" />

## Related [#related]

* [Choosing a layer](./choosing-a-layer.mdx) has the decision table from surface
  kind to rung, plus the nesting rules.
* [The contrast floor](./the-contrast-floor.mdx) explains why rungs 3, 4 and 5
  have the alphas they have.
* [Layering and elevation](../layering-and-elevation.mdx) has the z-index
  contract, which the ladder correlates with but does not define.
