Skip to content
Highlight

FoundationsUpdated Oct 3, 2026

Shadow

Shadow says how far off the page an element sits. One ramp, five steps, each an edge, a contact layer and an ambient layer; focus is one blue ring drawn everywhere by base.css.

Pick a level from what the element does, not from how it should look.

tsx
// Inline content gets a hairline, not a shadow<div className="bg-background rounded-xl border" /> // Pick the step from what the element does. The shadow draws the edge, so no border<div className="bg-background-raised shadow-sm rounded-xl corner-smooth" /> // Anything floating over the page: menus, selects, popovers<div className="bg-background-overlay shadow-lg rounded-2xl corner-smooth" /> // Focus is automatic. base.css rings every :focus-visible element; write nothing<button>Save</button> // The ring is an outline, so an element's own shadow stays through focus untouched<div className="shadow-lg" tabIndex={0} /> // A toned control keeps its tone through focus<Button tone="danger">Delete</Button>  // focus-visible:focus-ring-danger, built in
  • Inline content gets no shadow. A hairline border separates things on the page. Reach for shadow only when something sits above it.
  • A shadow is its own border. Every step starts with a 1px edge that flips to white on dark, where a black one would be lost. Pairing a step with border draws the edge twice. Menus, selects and popovers take shadow-lg.
  • Never write a focus ring. base.css draws it on every :focus-visible element, as an outline, so an element's own shadow-* is never in its way. A bare outline-none cannot remove it; only a focus-visible: utility can, which is how a toned control takes its colour.

Every level on the three surfaces it lands on. Toggle the theme to see which survive.

bg-background-raised

shadow-xs
shadow-sm
shadow-md
shadow-lg
shadow-xl

bg-background

shadow-xs
shadow-sm
shadow-md
shadow-lg
shadow-xl

bg-background-subtle

shadow-xs
shadow-sm
shadow-md
shadow-lg
shadow-xl

Which level a surface takes, by what the surface is.

SituationToken
Anything inlineNo shadow. A hairline border.
A resting card or input, barely off the pageshadow-xs
A raised element on a pageshadow-sm
A card that overlaps contentshadow-md
A menu, popover, dropdown or tooltipshadow-lg
A modal or command paletteshadow-xl

One ring, drawn as an outline: 3px of blue, offset 1px so the gap shows whatever sits behind.

focus-ring
focus-ring-danger
focus-ring-success
focus-ring

The ring is blue rather than the brand yellow because a yellow ring is all but invisible on white. The gap keeps it off the control, so it reads against a filled button as well as an outlined one, and since it is an empty offset rather than a painted band, it holds on a card, a subtle fill or a brand fill as well as on the page. Danger and success carry their own ring colour, so a toned button keeps its tone through focus; Button's on-light and on-dark tones take a fixed black or white one. Being an outline, the ring also survives forced-colors mode, which drops shadows.

Rich-text editors, options and menu items are excluded. The caret or the row highlight already marks focus there, and a ring on top would draw a second mark.

What the package defines.

css
/* packages/ui/src/styles/tokens.shadow.css: edge, contact, ambient */:root, .dark {  --shadow-edge: 0 0 var(--shadow-edge-blur) var(--shadow-edge-spread)		color-mix(in oklab, var(--gray-a-7) var(--border-ink), transparent);  --shadow-edge-firm: 0 0 var(--shadow-edge-blur) var(--shadow-edge-spread)		color-mix(in oklab, var(--gray-a-8) var(--border-ink), transparent);} :root {  --shadow-xs: var(--shadow-edge), 0 1px 2px 0 var(--black-2), 0 1px 4px 0 var(--black-2);  --shadow-sm: var(--shadow-edge), 0 1px 2px 0 var(--black-3), 0 2px 8px 0 var(--black-2);  --shadow-md: var(--shadow-edge), 0 2px 4px 0 var(--black-3), 0 6px 16px 0 var(--black-3);  --shadow-lg: var(--shadow-edge-firm), 0 4px 8px 0 var(--black-4), 0 12px 28px -2px var(--black-3);  --shadow-xl: var(--shadow-edge-firm), 0 8px 16px 0 var(--black-4), 0 24px 48px -4px var(--black-4);} .dark {  --shadow-xs: var(--shadow-edge), 0 1px 2px 0 var(--black-5), 0 1px 4px 0 var(--black-5);  --shadow-sm: var(--shadow-edge), 0 1px 2px 0 var(--black-6), 0 2px 8px 0 var(--black-5);  --shadow-md: var(--shadow-edge), 0 2px 4px 0 var(--black-6), 0 6px 16px 0 var(--black-6);  --shadow-lg: var(--shadow-edge-firm), 0 4px 8px 0 var(--black-7), 0 12px 28px -2px var(--black-6);  --shadow-xl: var(--shadow-edge-firm), 0 8px 16px 0 var(--black-7), 0 24px 48px -4px var(--black-7);} /* packages/ui/src/styles/tokens.resolution.css: the edge on a 2x screen; 1x swaps in a 0.5px ring at 60% ink */:root {  --border-ink: 100%;  --shadow-edge-blur: 1px;  --shadow-edge-spread: 0px;} /* packages/ui/src/styles/tokens.semantic.css */:root, .dark {  --focus-ring-color: var(--blue-9);  --focus-ring-alpha: 0.5;  --focus-ring: oklch(from var(--focus-ring-color) l c h / var(--focus-ring-alpha));  --focus-ring-width: 3px;  --focus-ring-offset: 1px;} .dark {  --focus-ring-alpha: 0.85;} /* base.css, on every :focus-visible element */outline: var(--focus-ring-width) solid var(--focus-ring);outline-offset: var(--focus-ring-offset);

shadow-xs to shadow-xl read these tokens and replace Tailwind's set. Bare shadow, shadow-2xl and shadow-inner do not exist here.