Surfaces

Eight-level surface and shadow ladder for elevation.
Light mode: two color steps then flat white, differentiated by shadow.
: additive white-opacity ladder with layered inset highlights and drops.

Installation

Tokens ship in app/globals.css. Once added, every bg-surface-N and shadow-surface-N utility (where N is 1–8) becomes available. The existing --background, --muted, and --card tokens are re-derived as aliases of --surface-1, --surface-2, and --surface-3.

Playground

Surface 3
Stack 1 - 3

The ladder

Surface 1App background
bg-surface-1shadow-surface-1aliased by --background
Surface 2Sunken / muted
bg-surface-2shadow-surface-2aliased by --muted
Surface 3Card
bg-surface-3shadow-surface-3aliased by --card
Surface 4Raised card
bg-surface-4shadow-surface-4
Surface 5Floating panel
bg-surface-5shadow-surface-5
Surface 6Dropdown / menu
bg-surface-6shadow-surface-6
Surface 7Popover
bg-surface-7shadow-surface-7
Surface 8Modal / dialog
bg-surface-8shadow-surface-8

Usage

Each surface level pairs a background color with a shadow recipe of matching elevation. Apply them together: className="bg-surface-3 shadow-surface-3".

In light mode, surfaces 3–8 share the same #FFFFFF background; the shadow alone communicates elevation. , each level adds a small amount of white opacity over #171717, and the shadow recipe layers an inset top-edge highlight, an inset border ring, an outer hairline, and stacked drop shadows.

Shadows compose additively — surface N + 1's recipe is surface N's recipe with one additional drop layer at the next halving offset. This makes the elevation walk smoothly across the full ladder.

Relative elevation

Elevated components don't pick a fixed surface level — they elevate relative to whatever they sit on. A Dropdown opened on the page background renders at one level; the same Dropdown opened inside a Dialog renders higher. Without this, nesting collapses (a popover inside a popover renders the same color as its parent and disappears).

The mechanism: SurfaceProvider declares the current substrate level via React context. useSurface() reads it (default 1, the page background). Each elevated component computes its level as substrate + offset and re-provides the new substrate to its children, so further nesting walks up the ladder.

On the page — substrate 1 → surface 3
Inside a popover — substrate 3 → surface 5
Inside a Dialog — substrate 5 → surface 7

Used by

Tabs (selected pill)absolute surface-4
Dropdownsubstrate + 2
Selectsubstrate + 2
ColorPicker (popover)substrate + 2
MobileDrawersubstrate + 2
Dialogabsolute surface-5
Tooltipinverted (foreground/background)