Shadows
Overview
Shadows express elevation — how far a surface sits above the page. A resting button barely lifts; a modal floats well clear. This is the first foundation whose values sit off the master scale (a shadow isn't a scale step), so it ships genuinely new tokens.
Five levels, named by size like Radius, with the elevation intent in a "use for"
recipe. The values mirror Tailwind v4's shadow ramp, so --shadow-md the token and
shadow-md the utility are the same thing — written in HSL to match the rest of the system and
paste straight into Figma.
Source of truth: tokens/semantic.css in the Monogem codebase.
Principles
- Elevation is meaning. A higher shadow says "more temporary, more interactive" — a dropdown or dialog, not a static panel. Match the level to the job.
- One neutral shadow. Pure black at low opacity, no color tint — the onyx system has no hue to lend a shadow.
- Same level, both themes. The geometry never changes between light and dark; only the opacity rises in dark, so an element reads as the same elevation either way.
- Borders help on dark. Where a shadow fades against near-black, a subtle
--bordercarries part of the depth — overlays pair the two.
Tokens
| Token | Use for |
|---|---|
--shadow-xs | Resting buttons, subtle lift |
--shadow-sm | Cards, raised panels |
--shadow-md | Dropdowns, popovers, hover |
--shadow-lg | Modals, dialogs, sheets |
--shadow-xl | Command palette, max overlay |
The geometry matches Tailwind v4 exactly; only the opacity differs between themes:
/* light */ /* dark — same shape, boosted opacity */
--shadow-md: 0 4px 6px -1px /0.10, --shadow-md: 0 4px 6px -1px /0.30,
0 2px 4px -2px /0.10; 0 2px 4px -2px /0.30;
In Tailwind these map to shadow-xs … shadow-xl — tokens and utilities never disagree. Full
values live in tokens/semantic.css.
Dark mode. Black at 10% is nearly invisible on a near-black surface, so
.darkre-declares each token at ~3× the opacity. Even boosted, shadows do less work on dark than light — pair overlay shadows (lg/xl) with a--borderso the edge stays defined.
Rationale
Why Tailwind's values? The same reasoning as the type scale and spacing: adopt the framework's
numbers and the token layer never disagrees with a utility. shadow-lg and --shadow-lg resolve
to the identical shadow.
Why HSL, not Tailwind's rgb? Every color in this system is written as HSL, and HSL pastes
straight into Figma variables. A shadow's color is just black — hsl(0 0% 0% / α) — so it follows
the same house rule.
Why five levels? Restraint. xs → xl covers the real ladder — resting control, card,
dropdown, modal, palette. Tailwind's 2xs and 2xl are range we don't need yet, and a missing
level is easy to add later.
Why boost opacity in dark instead of keeping the same values? shadcn's default reuses the
light shadows on dark, where they barely register against --neutral-950. Raising the alpha
(~3×) makes elevation perceivable again — an accessibility call, not decoration — while the
geometry stays fixed so levels read consistently across themes.
Why lean on borders too? Even boosted, a shadow spreads softly and can still read faintly on
dark. A 1px --border gives a crisp edge that a shadow can't, so the two together define an
overlay more reliably than either alone.
Do / Don’t
Do
- Use a
--shadow-*token; match the level to the component (cardsm, dropdownmd, modallg). - On dark, pair overlay shadows (
lg/xl) with a--borderfor a defined edge. - Step elevation one level at a time — neighbouring levels read as a clear, gentle lift.
Don’t
- Hand-write
box-shadowstrings — reach for a token so light/dark and Figma stay in sync. - Over-elevate: a resting card doesn't need
xl. Big shadows are for things that float above content. - Tint shadows with color — the system's shadow is neutral black only.