Elevation in dark mode: when drop shadows stop working
On a light background, depth is easy. Drop a soft shadow and the card lifts. In dark mode that same shadow nearly disappears, because there's no light surface for it to darken. You crank the opacity, it turns into a muddy black halo, and it still doesn't look raised. Dark UIs need a different model.
Five steps, resting to floating. The depth comes from four cues working together.
The surface
Before any ambient shadow, every elevated surface gets the same three layers:
box-shadow:
inset 0 1px 0 0 rgba(255,255,255,0.08), /* top light line */
inset 0 0 0 1px rgba(255,255,255,0.04), /* inner hairline */
0 0 0 1px rgba(0,0,0,0.16); /* outer edge ring */The inset top line fakes light catching the upper edge, and honestly it's the
single biggest trick for "raised" in dark mode. The inner hairline keeps the
fill from looking flat. The outer ring is a 1px dark separator so the surface
doesn't bleed into the near-black background behind it. Together they define a
plane, even at rest with no shadow at all.
Ambient shadows that double
Elevation is then just how many ambient layers you stack on top:
0 1px 1px -0.5px rgba(0,0,0,0.18)
0 3px 3px -1.5px rgba(0,0,0,0.18)
0 6px 6px -3px rgba(0,0,0,0.18)
0 12px 12px -6px rgba(0,0,0,0.18)Two details make this read as real light. The offset and blur double each step (1, 3, 6, 12), because a real penumbra grows faster than linearly with height, so doubling just feels right. And the spread goes negative (−0.5, −1.5, −3, −6), pulling each layer inward by half its blur. Skip that and the layers pile into one dark blob. Keep it and each stays a soft, distinct band. That's the whole gap between a smooth shadow and a drop shadow with the opacity turned up.
Level 1 is surface-only, a resting card. Each step up adds the next ambient layer, so level 5, a sticky or floating bar, carries all four.
Make it a token, not a one-off
Stash the surface in a custom property and append the ambient layers per level:
.dm-elev { --dm-surface:
inset 0 1px 0 0 rgba(255,255,255,.08),
inset 0 0 0 1px rgba(255,255,255,.04),
0 0 0 1px rgba(0,0,0,.16); }
.dm-elev-3 { box-shadow: var(--dm-surface),
0 1px 1px -0.5px rgba(0,0,0,.18),
0 3px 3px -1.5px rgba(0,0,0,.18); }Now elevation is a scale you reach for by name. level={3} for a popover,
level={5} for a floating bar, instead of hand-tuning shadows on every
component and hoping they stay consistent.
Grab the component and copy any level's stack on the Dark Elevation page.
Ask your agent to implement this
Read the full writeup at https://seangeng.com/writing/dark-mode-elevation.md and implement it in my project.
It covers: Elevation in dark mode: when drop shadows stop working. A single drop shadow vanishes on a dark background. Here's the layered box-shadow system that reads as real depth in dark UIs: a top light line, an inner hairline, an edge ring, and ambient shadows that double with negative spread.
Requirements:
- Follow the technique/approach exactly as described in the writeup.
- Adapt names, colors, and styling to my project's existing conventions.
- If it's a component, make it reusable with sensible props and TypeScript types.
- Keep it accessible: semantic HTML, keyboard support, and respect prefers-reduced-motion.
- When done, tell me which files you created or changed and how to use it.Paste into Claude Code, Codex, Cursor, or any agent. view raw .md download source .zip