Elevation and depth
Two separate systems, and confusing them is how a menu ends up under a table header: z-index says what is in front, shadow says what is floating. A surface can be one without the other.
The z ladder — every value in the system, and nothing between them
Read out of the components, not invented:
| z | What sits there | Why it is above the one below |
|---|---|---|
z-0 | A card's own click layer ( KpiCard's full-surface link) | Below its content, so a hoverable child inside a clickable card still works |
z-10 | Sidebar panel · pinned table body cell · focused toggle in a group | A pinned column floats over the rows it scrolls past |
z-20 | The page header (sticky) · plain sticky table header · sidebar drag strip | A sticky header must beat the rows under it |
z-30 | Pinned table header cell (sticky on both axes) | It must beat both the pinned column (10) and the plain header (20) |
z-40 | A full-surface "updating" overlay pinned over a panel ( presets) | Above the content it covers, below every portalled overlay |
z-50 | Dialog · Sheet · Dropdown menu · Select content · Popover | Every portalled overlay shares one layer; they never stack against each other |
z-[60] | The map's hover chip | Above the map's floating chrome, below the overlays |
z-[70] | The feedback aim ring | Above every screen it is drawn on, below the pill that explains the mode |
z-[71] | The feedback mode pill | The one element that says the mode is armed. It must never be the thing that is covered |
z-[100] | Tooltip (and its arrow) | Deliberate headroom over a table's own sticky max of 30 — a tooltip on a pinned header must not vanish under it |
Do not invent a value between these. The gaps are the headroom: they exist so a new sticky thing can slot in without a re-shuffle of everything above it.
Two rules that fall out of the ladder:
- The pinned-header rules are mutually exclusive by construction. A pinned
head cell takes a plain
z-30; writing a descendant rule as well would put a (0,2,0) container rule against a (0,1,0) utility and the pin would lose. - A pinned cell is
bg-inherit, so it keeps its row's zebra, hover and selected colour while it floats. A pinned cell with its own fill is a stripe that disagrees with its row.
Elevation — three tiers, and a resting surface has none
| Tier | Token | Value | What earns it |
|---|---|---|---|
Resting | --shadow-card | 0 1px 2px rgba(0,0,0,.04), 0 1px 6px rgba(0,0,0,.04) | A card. Barely there — the border is what defines it |
Overlay | --shadow-card-hover → shadow-overlay | 0 4px 12px rgba(0,0,0,.06), 0 12px 32px rgba(0,0,0,.06) | Popover, dropdown, select content, tooltip, floating map panel |
Modal | --shadow-modal | 0 24px 80px rgba(0,0,0,.18) | Dialog, Sheet |
In dark mode both overlay and modal resolve to --shadow-modal: a soft
shadow is invisible on charcoal, so the raised surface leans on the titanium
ramp (--surface-card a step lighter than the page) and takes the deeper
shadow to keep an edge.
Rules:
- A resting surface is a LINE, not a shadow. Cards sit on a 1px border. If you find yourself adding a shadow to make something "pop", the answer is usually contrast or spacing.
- Shadows are soft and warm — pure black at 4–18% alpha. Never a grey-blue shadow, never a hard offset.
- Elevation is not a hover state. A card that lifts on hover is a card that moves; the row ladder and the accent tint carry hover in this system.
- Reach them as
shadow-[var(--shadow-card)], or the mintedshadow-overlay/shadow-modalutilities.--shadow-card: var(--shadow-card)inside@themeis a self-reference that resolves to nothing (11-traps.md).
Shadow used as a BORDER — the inset family
Six of these exist and they are not elevation at all; they are hairlines that must not take part in layout:
| Utility | Where | Why a shadow and not a border |
|---|---|---|
shadow-[inset_-1px_0_0_var(--border)] | pinned-left table column | A real border would shift the cell's box and break column alignment while it floats |
shadow-[inset_1px_0_0_var(--border)] | pinned-right column | same |
shadow-[inset_3px_0_0_var(--foreground)] | selected table row | A 3px left bar with zero layout cost, and the second cue selection needs at 1.28:1 |
shadow-[inset_0_0_0_1px_var(--foreground)] | focus/selection ring inside a dense control | A ring that lives inside the box rather than outside it |
shadow-[inset_0_0_0_1px_var(--destructive)] | the Bug half of the feedback form’s type toggle | The same ring one hue over: selected still reads as selected, and the destructive tone says which half you picked without a second control |
shadow-[0_0_0_1px_var(--sidebar-border)] → shadow-[0_0_0_1px_var(--sidebar-accent)] on hover | the rail's outline button | A 1px "border" that can change colour on hover without a reflow |
shadow-none is also a real value: the line tab variant switches its active
state off the enclosed pill's shadow.
Layering surfaces without shadow
The system prefers a step in the surface ramp to a shadow:
- light: page cream
#FAF8F5→ card warm white#F5F3EF→ muted#F1EEE8 - dark: charcoal
#18181B→ titanium#2C2C2E→ titanium mid#3A3A3C→ edges#48484A
Three steps is the whole depth budget of a screen. If you need a fourth, the screen is doing too much.