Surfaces

An 8-step elevation ladder, plus a context-aware Surface Provider so every elevated primitive renders relative to its substrate — not against a hard-coded level that collapses the moment it nests.

Elevation ladder

Light theme keeps two color steps and differentiates levels 3–8 with stacked shadow drops. Dark theme walks the full luminance range from oklch(0.08) to oklch(0.32), layered with inset highlights and outer hairlines. Every level pairs a background with a shadow recipe — never use one without the other.

surface-1 Page background
surface-2 Sunken / muted
surface-3 Base elevated
surface-4 Raised card
surface-5 Modal overlay
surface-6 Raised modal
surface-7 Deep floating
surface-8 Maximum cap

Why relative elevation

Hard-coded surface levels collapse the moment a component nests inside something at the same level. Inside a surface-5 dialog, a popover hard-coded to bg-surface-5 literally disappears into the dialog. bloom-nx solves this with a Svelte context: components read their substrate from the nearest provider, lift by a per-primitive offset, then re-provide the new level so further nesting keeps lifting.

Conventional offsets

Two strategies set surface levels: relative (read substrate, add offset) and absolute (pin a level regardless of context).

PrimitiveStrategyResolves to
Card, Sidebar, Toolbar, Alert, Dockabsolutesurface-3 (Card accepts 3 | 4 | 5)
Popover, Dropdown, Context-Menu, Hover-Card, Select, Menubar, Navigation-Menurelative (offset +2)page → surface-3, card → surface-5, dialog → surface-7
Dialog, Sheet, Drawer, AlertDialogrelative (offset +4)page → surface-5, card → surface-7, feed (2) → surface-6

App shells: prototype layouts declare data-substrate="1" (page). Feed columns use <Substrate level={2}> — sunken band on the page; cards inside feed stay at surface-3.

Relative elevation, in practice

A dropdown opened over the page renders at surface-3 (page (1) + 2 = 3). Open that same dropdown inside a dialog (surface-5) and it shifts to surface-7. The shadow recipe and background color scale with the resolved level so depth reads accurately against its immediate context.

Page (substrate 1)

Card (substrate + 2 → surface 3)

Dialog (substrate + 2 → surface 5)

Popover (substrate + 2 → surface 7)

Elevated primitive

<Elevated> wraps the read-substrate / compute-level / apply-classes / re-provide-context pattern in one component. Pass offset for relative lift or level for an absolute pin. Optional shadowLevel keeps the shadow signature constant when only the background should track substrate.

offset 2 → surface 5

on substrate 3

level 5 → surface 5

absolute pin

bg 5, shadow 3

fixed shadow signature

Authoring an elevated primitive

Reach for <Elevated> first. Drop to the hook directly only when authoring a new primitive (e.g. wrapping a bits-ui content node). Always advertise the resolved level back to descendants via data-substrate={level} — a literal value (e.g. data-substrate="7") lies to children when the resolved level differs.

useSurfaceProvider(2) resolved against substrate 3 — renders at surface-5.

Level catalog

LevelRoleUsed by
surface-1Page backgroundapp shell main background
surface-2Sunken / mutedtracks, fills, in-range bands
surface-3Base elevatedcards, sidebars, alerts, popovers on page
surface-4Raised cardnested cards, avatar discs above card
surface-5Modal overlaydialogs, sheets, popovers over cards
surface-6Raised modalnested elements over dialogs
surface-7Deep floatingpopovers / menus over dialogs
surface-8Maximum capextreme nesting limit (clamped)

Card-on-card

A nested <Card.Root level={4}> sits on top of its parent card without the hierarchy collapsing. Pair this with the deliberately raised offset whenever a card contains another card.

Outer card

Default level 3 — absolute elevation pin.

Nested card

Level 4 — visibly raised against its parent.

API reference

Helpers exported from $lib/components/ui/surface.

PropTypeDescription
() => 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8Read the current substrate level. Returns 1 when no provider is set.
(level: 1..8) => SurfaceLevelPin an absolute level for descendants. Used by Card to seed context.
(offset: number) => SurfaceLevelRead substrate, add `offset`, clamp 1..8, re-provide. Conventional offsets: 2 for popover-family primitives.
(level: 1..8) => SurfaceLevelPin a level absolutely, ignore parent. `5` is the bloom-nx modal convention.
Record<SurfaceLevel, string>Tailwind-safe class literal map. Use this instead of templating `bg-surface-${level}` — the latter does not survive content extraction.
(bg: 1..8, shadow?: 1..8) => stringSame as `surfaceClass[level]` but lets the shadow level differ from the bg level — e.g. keep `shadow-surface-3` while bg tracks substrate.
{ offset?: number; level?: 1..8; shadowLevel?: 1..8; padded?: boolean }Sugar component that wraps the hook. Provide either `offset` (relative) or `level` (absolute). `padded` adds default `p-4`.