opsinjs
ComponentsSurfaces

Surface

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.

Preview

Theme
Density
Text
Status
surface · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/surface

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { Surface } from "@/components/ui/surface"
<Surface rung="sheet">Everything the surface holds</Surface>

When to use it

Use it when

  • Building a layered component at a defined rung: a custom panel, a floating bar or a persistent header.
  • Any surface that overlays scrolling content and must stay readable whatever passes beneath it.
  • Expressing depth through material rather than shadow, which is how this system shows layering.

Do not use it when

  • You need a bounded group of related content with padding and a border, on the page rather than over it.Use card instead.
  • You are building a bottom sheet with detents and drag.Use sheet instead.
  • You are building a modal that traps focus.Use dialog instead.
  • You are reaching for a fourth composited translucent layer on one screen, which stutters on mid-range Android.Use card instead.
  • A number a person must read is going on `sheet` or `overlay`. A health value never sits on a translucent rung above `raised`.Use card instead.

Anatomy

  1. SurfaceThe root. Resolves the rung's material properties, carries its shadow, and is position: relative, never isolation: isolate.Controlled by rung
  2. Surface.BackdropBlur and saturation over the page. Removed on an opaque rung, under reduced transparency, increased contrast, forced colours, without backdrop-filter.Controlled by opaque
  3. Surface.ScrimThe rung's tint at max(tint-alpha, minimum scrim opacity). On every rung, reachable by no prop, and why text stays readable.
  4. Surface.EdgeAn outline in the rung's border colour, widened under increased contrast. Draws nothing where a rung's border token is none.
  5. Surface.ContentEverything the surface holds. Last in document order, so it paints above the other three. Carries no padding.Controlled by children

Examples

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.

Theme
Density
Text
Status
surface-over-scrolling-content · base base · style base-lyraOpen under the product theme

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.

Theme
Density
Text
Status
surface-opaque-fallback · base base · style base-lyraOpen under the product theme

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 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 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 rule and its repair are stated on the API. No mix occurs on any element the component itself renders.

KeyActionNotes
TabPasses through the surfaceSurface is presentational, with no role, no focus and no tab stop.
Shift+TabPasses through the surfaceSame in reverse. A Surface never joins the focus order, at any rung.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor
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

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

API reference

Prop

Type

Generated from SurfaceProps in registry/bases/base/surface.tsx.

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 rule applies in full.

  • Card is a bounded content group with padding and a border, sitting on the page rather than over it.
  • Sheet is a Surface at the sheet rung with detents, drag and dismissal.
  • Dialog is a Surface at the sheet rung, under a scrim, with focus management.
  • Skeleton is often placed on a Surface, where reduced transparency and reduced motion interact.

On this page