---
title: "Materials"
description: "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."
url: "https://opsinjs.pensievelabs.org/foundations/materials"
source: "https://opsinjs.pensievelabs.org/foundations/materials.md"
section: "Foundations"
kind: "foundation"
reviewed: "2026-09-02"
reviewer: "design"
aliases: ["surfaces", "backdrop-filter", "frosted"]
---

> 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]

A *material* is the answer to two questions about a surface: what is it made of,
and what can you see through it. Most systems answer the first with a shadow
scale and never ask the second. opsinjs answers both together, because in a
product where somebody reads a blood pressure off a card, "what is behind the
card" is a safety question.

There are six materials. They are an ordered ladder, not a palette, and the
order is the amount of separation the surface claims from what is behind it:

<MaterialLadder />

Two things make this a foundation rather than a styling convention.

**Every translucent rung has a fixed minimum backing tint.** The opacity of a
translucent material is not a design parameter you tune until it looks right. It
is the smallest value at which text on that material clears the contrast floor
*against the worst possible backdrop*. The worst case includes a photograph, a
video, or a solid black. That calculation is the point of the whole section and
it has its own page: [The contrast floor](./the-contrast-floor.mdx).

**Every translucent rung has a documented opaque fallback.** Reduced
transparency, print and any browser without `backdrop-filter` all land on the
same fallback, and the fallback changes paint only. It never changes layout.
Forced colours is specified to land there as well and does not yet; the
preference table below says which of the three are real. See
[Reduced transparency](./reduced-transparency.mdx).

The thing this is most often confused with is **elevation**. Elevation is
*order*: which thing is in front. Material is *appearance*: what the thing is
made of. They correlate, because higher things tend to be more translucent. The
two are nonetheless separate contracts, and the stacking rules live in
[Layering and elevation](../layering-and-elevation.mdx). A modal is above a card
because of its z-index, not because of its blur.

## How it works [#how-it-works]

### The properties of a rung [#the-properties-of-a-rung]

Every rung resolves the same eight custom properties, and there is no ninth:

```text
--opsin-material-<rung>-tint         the backing colour
--opsin-material-<rung>-tint-alpha   how opaque that tint is
--opsin-material-<rung>-blur         the backdrop-filter blur radius
--opsin-material-<rung>-saturation   the backdrop-filter saturation multiplier
--opsin-material-<rung>-border       the boundary, or `none`
--opsin-material-<rung>-shadow       the drop shadow, or `none`
--opsin-material-<rung>-scrim        the minimum scrim opacity text on this rung needs
--opsin-material-<rung>-opaque       the substitute used when translucency is off
```

`<rung>` is a name and never an index. The names are `canvas`, `card`, `raised`,
`sheet`, `overlay` and `scrim`. A number invites arithmetic, "one more than a
card", and the ladder is not arithmetic: each rung answers a different question
about what is behind it. [The ladder](./the-ladder.mdx) is the six questions and
the job description for each, and
[ADR 0014](../../project/decisions/0014-material-rung-names.mdx) is why these are
the names.

Constraining a material to one closed set is deliberate, and so is fixing all
eight per rung rather than exposing them for tuning. A system where a surface
may vary its tint, blur, saturation, border, shadow, inset highlight and noise
screen by screen has no ladder at all. It has a parameter space, and every
screen ends up somewhere different in it.

### The `Surface` primitive [#the-surface-primitive]

The way to consume a rung in product code is the
[Surface](../../components/surface.mdx) component, which takes the rung by name
and applies its properties together, so that a surface cannot be assembled
half-way. It is built and `shipped`, which means the API may still move; the
ladder it resolves will not.

A stylesheet or a component of your own can apply a rung without `Surface`.
Where that happens, apply the properties together and treat "together" as the
rule. A rung's blur without its backing tint is precisely the failure mode the
contrast floor exists to prevent.

### The three preference queries [#the-three-preference-queries]

The ladder is specified to answer three declared preferences, each with a
different answer rather than a shared kill switch. One of the three is
implemented:

| Preference                             | What changes                                                                                    | Implemented?                                                                                                                                                                                                                                                                                                                                                 |
| -------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `prefers-reduced-transparency: reduce` | translucent rungs collapse to their opaque fallback, drop their blur and reset their saturation | yes, in the token layer, for all six rungs                                                                                                                                                                                                                                                                                                                   |
| `prefers-contrast: more`               | borders strengthen and tints deepen; blur is unaffected                                         | no. `Surface` alone answers it, in its own utilities: it collapses the tint to the opaque fallback and widens the edge, and it does not deepen a tint or change a border colour. See [Increased contrast](../../accessibility/increased-contrast.mdx)                                                                                                        |
| `forced-colors: active`                | the palette is replaced by the operating system's; the ladder survives as borders alone         | partly. `Surface` draws its edge as an outline, which forced colours keeps and recolours to `CanvasText`, and it stops rendering the backdrop; the scrim's dimming is still lost, because the system palette replaces the tint with `Canvas`, and no forced-colours capture exists yet. See [Increased contrast](../../accessibility/increased-contrast.mdx) |

The two partially answered rows stay on the page because the specification is
worth publishing even where the answer is incomplete; the third column is there
so that nobody plans a translucent sheet under forced colours believing its
dimming survives, because `Surface`'s edge now does and the scrim's dimming does
not.

Print is a fourth case and behaves like the first, with the additional rule that
shadows and blur are removed rather than approximated. That one is real but it
is site chrome, not a token: `app/globals.css` drops `backdrop-filter` and every
shadow on paper, and a consumer's own build has to carry the same rule itself.

## Using it [#using-it]

**Pick a rung by job, not by looks.** "This needs to feel light" is not a reason.
"This is persistent chrome that content scrolls under" is.
[Choosing a layer](./choosing-a-layer.mdx) is the decision table, and it is
short on purpose.

**Never nest a translucent rung inside another.** Two blurs stacked is twice the
compositing cost and an unpredictable composite, and the contrast maths no
longer has a bounded worst case. A card inside a sheet is an opaque rung.

**Do not use more than three composited surfaces at once.** This is a
performance budget with a real reason behind it and a real way to measure it.
[Performance budget](./performance-budget.mdx) has both.

**Health values go on opaque rungs.** A number somebody may act on does not sit
on glass. The one narrow exception, and its condition, is on
[The contrast floor](./the-contrast-floor.mdx).

**Do not author alpha inline.** `oklch(1 0 0 / 0.6)` written into a component is
a safety parameter set by somebody who was not thinking about safety. The alpha
is a token because it is a floor.

## Tokens [#tokens]

<TokenTable scope="material" />

## Accessibility impact [#accessibility-impact]

Materials are where a design system most often ships an accessibility failure
that looks like a feature. Four specific obligations:

* **Text contrast over an unknown backdrop (WCAG 2.2 SC 1.4.3).** A translucent
  surface makes contrast a function of whatever happens to be behind it. The
  minimum backing tint converts that into a bounded worst case, which is the only
  way the requirement can be met at all rather than met on the screens somebody
  happened to test.
* **Non-text contrast (SC 1.4.11).** A hairline border at low alpha over a
  translucent surface is the classic way a card boundary disappears. Borders on
  translucent rungs are measured against the same worst-case composite as text.
* **Reduced transparency.** It is a stated operating-system preference, not a
  hint, and the response is total: the rung becomes opaque, not slightly less
  transparent.
* **Motion and vestibular safety.** Materials frequently arrive with a
  transition. Anything that scales or translates a large blurred surface is
  covered by [Reduced motion](../motion/reduced-motion.mdx); the material itself
  never animates its blur radius.

<ContrastReport scope="materials" />

## Related [#related]

* [The contrast floor](./the-contrast-floor.mdx) has the calculation that fixes
  every translucent rung's minimum tint, and the CI job that enforces it.
* [Layering and elevation](../layering-and-elevation.mdx) is the stacking
  contract, which is a different question from what a surface is made of.
* [Colour](../colour/index.mdx) has the tokens a material's content is coloured
  from, and the pair rule those tokens obey.
