Semantic shadow tokens for elevation, component states, and depth.
Create UI ships one shadow system. Every shadow is a token, applied with a single utility class that carries its full layer stack. You name the role you need (shadow-neutral-md, shadow-component-primary-hover, text-shadow-sm) instead of hand-writing a multi-layer box-shadow and a separate dark-mode override.
Why shadow tokens
A convincing shadow is never one layer. Elevation is a stack of casts at different blurs and offsets, and on a dark surface a black shadow vanishes, so you keep a dark-mode copy in sync too. The same raised card, written two ways:
The payoff:
- Depth as a name.
shadow-neutral-mdsays "raised card." A raw layer stack makes the next reader reverse-engineer the intent. - Dark mode for free. The tint lives in a color token that already flips, so a shadow stays believable on a dark surface without a
dark:override. - One ramp, even steps. A fixed
2xs → 3xlscale keeps the jump between elevations consistent across the whole product. - States without the math. Focus rings, hover glows, and inset borders are tokens too (
shadow-component-*), so a control's whole outline is a few class names instead of hand-tunedbox-shadowstacks.
The three roles
- Elevation (
shadow-neutral-*): soft drop shadows that lift a surface off the page. Cards, popovers, dropdowns, and modals climb this ramp. - Component (
shadow-component-*): inset borders and focus rings that draw the outline of an interactive control across itsdefault,hover, andfocusedstates. Solid controls also carry atext-shadow-2xson their label, so one button shows both shadow kinds at once. - Text (
text-shadow-*): a small ramp (2xs,xs,sm,md,lg) for legibility on text that sits over a colored or photographic fill.
All three are color-aware: the tint comes from separate shadow color tokens, which is what lets a single class work in both themes. More on that under Color and dark mode.
Usage
Apply a token class to any element. The class sets the whole shadow.
Unlike spacing and typography, shadows are fixed at every breakpoint. Depth and focus rings read the same on a phone and a desktop, so there is nothing responsive to manage.
Elevation
The neutral ramp is one continuous scale from a 1px hairline to a deep, far-cast shadow. Pick the step by how far off the page the surface should feel: a resting card sits low, a popover floats above it, a modal sits highest.
Component
Component shadows draw the outline of an interactive control. Each one stacks an inset ring (the border), a 2px inset highlight (the top bevel), and, on the raised states, an outer glow or focus outline. They come as a matrix of six families across three states.
The specimen below renders the real Button, pinned to each state, so every swatch shows the box shadow and the label's text-shadow-2xs at the same time.
The three states map to interaction:
default. The resting outline: a 1px inset ring plus a subtle inset highlight.hover. The ring brightens and a soft outer glow appears, so the control reads as liftable.focused. An outer outline ring is added on top of the default, signalling keyboard focus.
The specimen shows primary and neutral. The same default/hover/focused matrix exists for inverted, error, success, and info (for example shadow-component-error-hover), each tuned to its own fill.
These are usually wired into a component once, with the state classes layered through Tailwind variants:
Text
Text shadows add a touch of depth for legibility, most often on label text that sits over a colored or photographic fill. The ramp runs from a 1px seam to a soft three-layer drop. Solid buttons already use the smallest step (text-shadow-2xs) on their label.
Color and dark mode
Elevation shadows do not hard-code their color. They reference shadow color tokens, and those tokens are what invert between themes:
In light mode the shadow is a soft black alpha, so it reads as a cast shadow. In dark mode it resolves to a neutral tone, so depth stays visible instead of vanishing into the dark surface. You set the theme once at the root and every shadow follows. See the Colors page for the full token reference.
Under the hood
The shadow system is declarative, with a single source of truth:
registry/shadows.tsdefinesshadowTokens(theneutralandcomponentfamilies) andtextShadowTokensas plain data, each with alightanddarkvalue.buildShadowCssVars()andbuildTextShadowCssVars()inregistry/config.tsturn each token into a CSS variable.styles/globals.cssdeclares those--shadow-*and--text-shadow-*variables on:root, overrides the underlying color tokens under.dark, and registers everything in@theme inline.- Tailwind v4 generates the
shadow-*andtext-shadow-*utilities from the theme.
Because the tint lives in separate color tokens, the shadow definitions themselves are identical in light and dark. Only the color flips, which is why a single class covers both themes.
Shadows are not in the responsive token set (text, spacing, radius). They emit one flat value to :root with no breakpoint overrides, so depth is constant across screen sizes.
When a project runs createui init, the CLI emits the same variables into the consumer's stylesheet, so the shadow system travels with the components.