Skip to content

Elevation

Six levels, each mapped to a z-index band and a purpose. In light themes elevation is a shadow; in dark themes it is a lighter surface — the token name hides the difference.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Six levels
e0
z 0
e1
z 1
e2
z 10
e3
z 50
e4
z 65–75
e5
z 75–96

Why dark mode is not an inversion

The same four levels in both themes. Notice that the dark column changes background colour and the light column changes shadow — the token name is identical in both.

dark

canvas
surface
raised
overlay

Depth comes from the surface getting lighter. The shadow is nearly invisible and only sharpens the edge.

light

canvas
surface
raised
overlay

Depth comes from the shadow. The surface stays white at every level, so lightness cannot carry it.

Elevation as feedback

Hovering raises a card by one level and 1px of translation. Pressing drops it below its resting level. The movement is what makes the surface feel physical.

Hover me

e1 → e3 → e0

Static card

No shadow at all

Interaction states

Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.

e0Flush
e1Resting control
e2Liftable card
e3Popover
e4Toast, drawer
e5Dialog
GlowFocus emphasis
InsetA well, not a lift

Anatomy

Every part, every measurement, and the reason it is that number.

Level 3 surface

Two shadows: a tight contact shadow and a wide ambient one.

1 · contact

0 4px 8px −4px / 60%

2 · ambient

0 12px 20px −6px / 44%

3 · surface lift

surface → raised

4 · hairline

1px border-subtle

Every level is two shadows plus a surface change plus a hairline. Any one of them alone looks wrong.

  1. Contact shadowsmall blur, negative spread

    Tight and dark, directly under the element. This is what says "resting on" rather than "floating above". Without it, surfaces look like stickers.

  2. Ambient shadowlarge blur, larger offset

    Soft and wide. Communicates height. Growing this one alone is what makes an element seem to fly rather than lift.

  3. Surface step--ds-surface → --ds-surface-raised

    In dark mode this carries almost all of the perceived depth. Roughly 4–6% lightness per level is the range that reads as a step without looking like a different colour.

  4. Hairline border1px --ds-border-subtle

    Defines the edge where the shadow is too soft to. Essential in dark mode, where a shadow against near-black is barely visible.

  5. Z-index bande3 → z-50

    Bound to the level, not chosen per component. This is the discipline that prevents z-index: 9999 from ever appearing in the codebase.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed for
--ds-surface—Level 0–1 background
--ds-surface-raised—Level 2–3 background
--ds-surface-overlay—Level 4–5 background
--ds-border-subtle—The hairline that defines every elevated edge
--ds-layer-scrim—The dimming layer beneath a modal surface

Shadow

TokenValueUsed for
--shadow-e0—Flush. Explicit "no elevation".
--shadow-e1—Resting buttons and small controls
--shadow-e2—Interactive cards, sticky headers
--shadow-e3—Popovers, menus, tooltips, FAB
--shadow-e4—Toasts, drawers
--shadow-e5—Dialogs, sheets, command palette
--shadow-glow—Accent emphasis on a focused surface

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizeHeightWhen to use
Level 0z-0In-flow content. Table rows, list items, page sections.
Level 1z-1Controls at rest — buttons, switch knobs, kbd.
Level 2z-10Cards that respond to hover, sticky headers, table header rows.
Level 3z-50Popovers, dropdown menus, tooltips, floating action buttons.
Level 4z-65 – z-75Toasts, drawers, split-button menus, scrims.
Level 5z-75 – z-96Dialogs, bottom sheets, the command palette.
Inspectorz-200+Development overlays only. Never used by product UI.

Do

e2 → z-10 · sticky headere3 → z-50 · popovere4 → z-70 · scrime5 → z-75 · dialog
Bind z-index to the elevation levelSix levels, six bands, no arguments. Anyone can work out where a new surface belongs without reading the rest of the codebase or inventing a bigger number.
surface + e1
raised + e3
overlay + e5
Change surface colour and shadow togetherBoth channels carry depth, and which one dominates depends on the theme. Moving them in lockstep means the same component reads correctly in dark and light without a second design.
hover: e1 → e2
Move one level on hover, not threee1 → e2 reads as "this responds". e1 → e5 reads as an animation glitch. The size of the change should be proportional to the significance of the interaction.
Dialog
Pair every modal elevation with a scrimThe shadow says "above"; the scrim says "the thing underneath is not available". Users need both to understand that the page is paused rather than merely obscured.

Don't

Do not elevate everythingIf every card floats, none of them do. Elevation is a relative signal — it only means something when most of the page is flat.
Do not use a light-mode shadow in dark modeA soft grey shadow on a near-black surface fogs the edge instead of defining it. Dark shadows are tighter, darker, and paired with a hairline border.
z-index: 999999; /* above the other 99999 */
Do not raise z-index to fix a stacking bugz-index: 9999 is a symptom of a missing portal or an ancestor with a transform. The fix is to render the surface at the body, not to outbid the last person who had this problem.
Important announcement, apparently floating over the page
Do not use elevation for emphasisA raised element reads as temporary and dismissible. Making an important static section float tells the user it will disappear if they click elsewhere.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.4.11Non-text ContrastAA1.4.1Use of ColorA

Contrast

  • A shadow is not a boundary for contrast purposes. Every elevated surface also needs a 1px border or a surface-colour difference that reaches 3:1 against what is behind it.
  • In Windows High Contrast Mode shadows are removed entirely. If depth was carried only by shadow, the layering disappears — which is why the hairline border is mandatory.
  • The scrim must be dark enough to make background text clearly inactive, and light enough that the user can still see the context. 72% in dark, 42% in light.

Keyboard

TabFocus order must match visual stacking. A dialog at e5 traps focus; nothing behind it should be reachable.
EscDismisses the topmost elevated surface, one level at a time.

Screen readers

  • Shadows are never announced. Stacking must be expressed through aria-modal, inert, and DOM order.
  • A popover rendered in a portal is far from its trigger in the DOM — wire it with aria-controls and aria-expanded so the relationship survives.

Focus & touch

  • Elevated surfaces must receive focus when they open and return it to the trigger when they close. Elevation without focus management is a visual change with no accessibility meaning.
  • Elevated surfaces on touch need a larger dismissal area: the scrim itself should be tappable, and a bottom sheet should support drag-to-dismiss as well as a close control.
AttributeApplied toNotes
aria-modal="true"Dialogs and drawers at e5Marks everything beneath as inert to assistive tech. Visual elevation alone conveys nothing to a screen reader.
inertBackground contentThe modern equivalent of aria-hidden plus a focus trap; removes the subtree from focus and from the accessibility tree.
role="tooltip" / "menu"e3 surfacesNon-modal elevated surfaces need a role so their relationship to the trigger is expressed structurally.

Code

Example usage

tsx
1// Elevation is a token, never a literal box-shadow2<Card className="shadow-e2" />3<Popover className="shadow-e3" />4<Dialog className="shadow-e5" />56// Hover moves exactly one level7<div className="shadow-e1 transition-shadow duration-[180ms] hover:shadow-e2" />89// Surface and shadow move together10<div className="bg-overlay shadow-e5 border border-line rounded-2xl" />1112// Z-index comes from the level, not from a guess13const Z = {14  base:    0,15  sticky:  10,   // e216  popover: 50,   // e317  scrim:   70,   // e418  modal:   75,   // e519  toast:   90,20  palette: 96,21} as const2223// Modal surfaces render at <body> so no ancestor transform can clip them24createPortal(<Dialog />, document.body)

CSS

Two shadows per level in both themes. Note how much darker and tighter the dark-mode values are.

css
/* DARK — tight, near-black, paired with a lighter surface */
:root, [data-theme='dark'] {
  --ds-shadow-1: 0 1px 2px -1px rgb(0 0 0 / 0.60),
                 0 1px 1px      rgb(0 0 0 / 0.32);
  --ds-shadow-3: 0 4px 8px -4px rgb(0 0 0 / 0.60),
                 0 12px 20px -6px rgb(0 0 0 / 0.44);
  --ds-shadow-5: 0 16px 32px -8px  rgb(0 0 0 / 0.66),
                 0 40px 72px -16px rgb(0 0 0 / 0.60);
}

/* LIGHT — softer, wider, lower alpha */
[data-theme='light'] {
  --ds-shadow-1: 0 1px 2px rgb(16 18 22 / 0.06),
                 0 1px 3px rgb(16 18 22 / 0.05);
  --ds-shadow-3: 0 4px 8px -4px  rgb(16 18 22 / 0.09),
                 0 12px 24px -6px rgb(16 18 22 / 0.09);
  --ds-shadow-5: 0 16px 32px -8px  rgb(16 18 22 / 0.12),
                 0 40px 80px -16px rgb(16 18 22 / 0.16);
}

/* High Contrast Mode strips shadows — the hairline is the fallback */
@media (forced-colors: active) {
  .elevated { border: 1px solid CanvasText; }
}

/* Animate shadow, never spread. Spread triggers paint on every frame. */
.card {
  transition: box-shadow 180ms var(--ease-standard),
              transform  180ms var(--ease-standard);
}

Notes

Professional tips

  • If you are unsure which level something needs, ask how the user dismisses it. Nothing to dismiss is e0–e2; click-outside is e3; Escape and a scrim is e4–e5.
  • A sticky header only needs its shadow once content has scrolled under it. Toggle the class on an IntersectionObserver sentinel rather than showing it permanently.
  • For drag and drop, jump the dragged item to e4 and drop the placeholder to e0. The gap between them is what makes the drag feel like lifting.
  • Two shadows always beat one. A single large blur looks like a glow; the tight contact shadow is what sells contact with the surface below.

Performance

  • box-shadow is painted on the CPU and repaints whenever the element moves. For anything animating position, prefer a pre-rendered shadow on a composited layer, or animate opacity between two stacked shadows.
  • Large blur radii are expensive in proportion to blur², not to element size. An 80px blur on a full-screen dialog is one of the most costly paints in a typical app.
  • backdrop-filter combined with a shadow forces two separate compositing passes. It is fine on one dialog and a real problem on fifty list rows.
  • will-change: transform on a hover-elevating card promotes it to its own layer, but do not leave it on permanently — each layer costs GPU memory.

Common mistakes

  • Elevating a surface without changing its background. In dark mode the shadow is nearly invisible, so the element appears completely flat.
  • Rendering a popover inside a container that has a transform. The transform creates a containing block and position: fixed silently stops being fixed.
  • Using the same elevation for a hover state and a dragging state, so users cannot tell whether they have picked something up.
  • Putting a shadow on a full-width sticky bar. The shadow is only visible at the edges and reads as a rendering artefact.

Real-world recommendations

  • Export your z-index bands as a TypeScript const and forbid literal z-index values in review. It eliminates an entire category of bug permanently.
  • Test every elevated surface in Windows High Contrast Mode. Shadows vanish, and any layering that depended on them alone disappears with them.
  • On mobile, prefer full-screen or edge-anchored surfaces to floating ones. A floating dialog on a 375px screen wastes the margins and puts controls out of thumb reach.
  • When a designer asks for "more depth", the fix is usually more contrast between adjacent surface colours, not a bigger shadow. Bigger shadows read as blur, not as height.