---
title: "Surface"
description: "The primitive behind every panel, sheet and card. It decides how solid or translucent a layer looks, and guarantees that whatever sits on it stays readable."
url: "https://opsinjs.pensievelabs.org/components/surface"
source: "https://opsinjs.pensievelabs.org/components/surface.md"
section: "Components"
status: "shipped"
kind: "component"
category: "surfaces"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["material", "blur", "translucency", "glass", "vibrancy", "elevation", "layer"]
usedIn: ["choose-a-component", "daily-log-screen", "diabetes-medicines-app", "health-metric-card", "onboarding-screen", "results-screen", "trends-screen"]
implemented: true
---

> 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.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="surface"
  status="shipped"
  questions="[
  &#x22;Nothing measures the tint composited at its published floor, so every figure here is a rung's opaque fallback and `minScrimOpacity` is asserted rather than derived.&#x22;,
  &#x22;Whether `prefers-contrast: more` belongs in this component at all, since the edge widens and keeps its published alpha against an unmeasured non-text floor.&#x22;,
  &#x22;Nobody has opened the site under a real Windows contrast theme, and the scrim's dimming does not survive there because the palette replaces the tint with Canvas.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="surface" />

## Installation [#installation]

<ComponentInstall name="surface" unbuilt="false" importPath="@/components/ui/surface" />

## Usage [#usage]

```tsx
import { Surface } from "@/components/ui/surface"
```

```tsx
<Surface rung="sheet">Everything the surface holds</Surface>
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;Building a layered component at a defined rung: a custom panel, a floating bar or a persistent header.&#x22;,
  &#x22;Any surface that overlays scrolling content and must stay readable whatever passes beneath it.&#x22;,
  &#x22;Expressing depth through material rather than shadow, which is how this system shows layering.&#x22;,
]"
  avoid="[
  { case: &#x22;You need a bounded group of related content with padding and a border, on the page rather than over it.&#x22;, instead: &#x22;card&#x22; },
  { case: &#x22;You are building a bottom sheet with detents and drag.&#x22;, instead: &#x22;sheet&#x22; },
  { case: &#x22;You are building a modal that traps focus.&#x22;, instead: &#x22;dialog&#x22; },
  { case: &#x22;You are reaching for a fourth composited translucent layer on one screen, which stutters on mid-range Android.&#x22;, instead: &#x22;card&#x22; },
  { case: &#x22;A number a person must read is going on `sheet` or `overlay`. A health value never sits on a translucent rung above `raised`.&#x22;, instead: &#x22;card&#x22; },
]"
/>

## Anatomy [#anatomy]

<Anatomy
  name="surface"
  parts="[
  {
    name: &#x22;Surface&#x22;,
    describes: &#x22;The root. Resolves the rung's material properties, carries its shadow, and is position: relative, never isolation: isolate.&#x22;,
    prop: &#x22;rung&#x22;,
  },
  {
    name: &#x22;Surface.Backdrop&#x22;,
    describes: &#x22;Blur and saturation over the page. Removed on an opaque rung, under reduced transparency, increased contrast, forced colours, without backdrop-filter.&#x22;,
    prop: &#x22;opaque&#x22;,
  },
  {
    name: &#x22;Surface.Scrim&#x22;,
    describes: &#x22;The rung's tint at max(tint-alpha, minimum scrim opacity). On every rung, reachable by no prop, and why text stays readable.&#x22;,
  },
  {
    name: &#x22;Surface.Edge&#x22;,
    describes: &#x22;An outline in the rung's border colour, widened under increased contrast. Draws nothing where a rung's border token is none.&#x22;,
  },
  {
    name: &#x22;Surface.Content&#x22;,
    describes: &#x22;Everything the surface holds. Last in document order, so it paints above the other three. Carries no padding.&#x22;,
    prop: &#x22;children&#x22;,
  },
]"
/>

## Examples [#examples]

### Chrome that content scrolls beneath [#chrome-that-content-scrolls-beneath]

`rung="overlay"` doing the job it is named for. The panel opens already scrolled,
so a dark row sits under the pinned header and the scrim holds the heading
legible over it.

<ComponentPreview name="surface-over-scrolling-content" kind="example" align="start" />

### The degraded path, without changing a setting [#the-degraded-path-without-changing-a-setting]

`opaque` renders the same fallback that reduced transparency, `prefers-contrast:
more` and a missing `backdrop-filter` reach. The border, shadow and geometry
stay, because they carry the layering once translucency goes.

<ComponentPreview name="surface-opaque-fallback" kind="example" align="start" />

## Accessibility [#accessibility]

**Audited against WCAG 2.2 AA.** A source pass over the full rubric and a
rendered pass on `/view/base/base-lyra/component/surface`. This audit is
author-run, so it is not an independent accessibility review, and clinical
review is still pending. The rendered pass reported zero axe violations, no
target-size hits, no focus-visible findings, and no horizontal scroll at 320px.
The audit changed nothing in the source, because the source already met the
checks: the surface is presentational with no role, no name and no tab stop, its
three decorative layers are empty and `aria-hidden`, the Backdrop renders only
in the branch where the Scrim is unconditional so no text sits over an unscrimmed
blur, and reduced transparency, `prefers-contrast: more`, `forced-colors: active`
and a missing `backdrop-filter` each reach a defined
[opaque fallback](../accessibility/reduced-transparency.mdx) that keeps the
border, shadow and geometry. The boundary is an outline, so `forced-colors`
preserves it where a shadow would be dropped.

**Two residual items belong to the token owner, not to this file.** Under
`prefers-contrast: more` the edge [widens](../accessibility/increased-contrast.mdx)
from a hairline to an emphasis width but keeps its published
`--opsin-material-*-border` colour, and several rungs publish a low alpha, so
whether the widened edge clears 3:1 for SC 1.4.11 is unmeasured. Substituting a
solid colour would give `canvas` and `scrim` an edge they are specified not to
have, so the fix sits in `tokens/material.json`, not here. Separately, `className`
is merged unrestricted, so a caller can put a category tint and a status tint on
one Surface. The component cannot detect that at render time, and the
[two colour axes](../health/two-colour-axes.mdx) rule and its repair are stated
on the API. No mix occurs on any element the component itself renders.

<KeyboardTable
  name="surface"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Passes through the surface&#x22;,
    notes: &#x22;Surface is presentational, with no role, no focus and no tab stop.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Passes through the surface&#x22;,
    notes: &#x22;Same in reverse. A Surface never joins the focus order, at any rung.&#x22;,
  },
]"
/>

<ContrastReport component="surface" />

## API reference [#api-reference]

<PropsTable name="SurfaceProps" />

`rung` is required, because no default depth is safe to guess. There is no
`blur`, `opacity`, `tint`, radius or `as` prop, and no `contentWeight`, because
no large-text scrim floor has been measured. A rung outside the six is refused.
Development warns and the children still render, with no material. `className`
is merged unrestricted, so the [two colour axes](../health/two-colour-axes.mdx)
rule applies in full.

## Related [#related]

* [Card](./card.mdx) is a bounded content group with padding and a border, sitting on the page rather than over it.
* [Sheet](./sheet.mdx) is a Surface at the sheet rung with detents, drag and dismissal.
* [Dialog](./dialog.mdx) is a Surface at the sheet rung, under a scrim, with focus management.
* [Skeleton](./skeleton.mdx) is often placed on a Surface, where reduced transparency and reduced motion interact.
