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.
// 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
borderseparates 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
borderdraws the edge twice. Menus, selects and popovers takeshadow-lg. - Never write a focus ring.
base.cssdraws it on every:focus-visibleelement, as an outline, so an element's ownshadow-*is never in its way. A bareoutline-nonecannot remove it; only afocus-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-xsshadow-smshadow-mdshadow-lgshadow-xlbg-background
shadow-xsshadow-smshadow-mdshadow-lgshadow-xlbg-background-subtle
shadow-xsshadow-smshadow-mdshadow-lgshadow-xlWhich level a surface takes, by what the surface is.
| Situation | Token |
|---|---|
| Anything inline | No shadow. A hairline border. |
| A resting card or input, barely off the page | shadow-xs |
| A raised element on a page | shadow-sm |
| A card that overlaps content | shadow-md |
| A menu, popover, dropdown or tooltip | shadow-lg |
| A modal or command palette | shadow-xl |
One ring, drawn as an outline: 3px of blue, offset 1px so the gap shows whatever sits behind.
focus-ringfocus-ring-dangerfocus-ring-successfocus-ringThe 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.
/* 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.