Lumen
Styles

Space & shape

A 4px spatial base, real control sizes, one stacking vocabulary, a radius scale, three elevations, and motion.

Spacing — 4px base

All spacing is a multiple of 4px, and the base is a token — --spacing: 0.25rem — so Tailwind's spacing utilities (p-4, gap-2, …) derive from Lumen, not from a coincidence. The common rungs:

4 · 8 · 12 · 16 · 24 · 32 · 48 · 64

Size — controls & the tap floor

The dimensional decisions the kit actually ships, as tokens (use h-(--size-control-md) etc.):

TokenValueUse
--size-control-sm32pxCompact rows (Button sm)
--size-control-md36pxThe default control (Button md, Input)
--size-control-lg44pxHero / touch-first (Button lg)
--size-tap-min24pxThe hard floor — no pressable target below this (WCAG 2.5.8)

Layers — one stacking vocabulary

Z-index is a scale, not an arms race. Use z-(--layer-modal) etc.; never a bare number.

TokenValueWhat lives there
--layer-raised10Lifted cards, sticky affordances inside content
--layer-sticky20Sticky headers/rails
--layer-overlay40Scrims (the --overlay wash)
--layer-modal50Sheets, dialogs
--layer-popover60Menus, selects, popovers — may open above a modal
--layer-toast70Toasts — outlive the modal that spawned them
--layer-tooltip80Tooltips — beat everything

Radius

The rounded-* utilities are bound to this scale (not Tailwind's defaults — Tailwind's md is 6px, Lumen's is 12px). Each step has one job.

TokenValueUse
sm8pxChips and small controls
md12pxInputs and buttons
lg20pxCards
xl28pxSheets and dialogs
full999pxPills, dots, avatars, the tab bar

Strokes

TokenValueUse
--stroke-hairline1pxDividers inside a card, and the edge of a control
--stroke-emphasis2pxFocus rings, invalid borders

Elevation

Depth comes from fill and shadow, not from lines. A card stands apart from the ground by being one step lighter and carrying shadow-sm; it has no border. Three warm-ink shadows, plus the one scrim. shadow-sm / shadow-lg / shadow-xl utilities resolve to these. At Night shadows barely read, so surfaces also step lighter as they rise (--surface-raised).

Each shadow lists its wide layer first. On iOS and Android, NativeWind draws only a shadow's first layer, so that layer must carry the lift. A view that clips its content, with overflow-hidden, loses its shadow on iOS, so a clipped card keeps its line instead. So does a card that sits inside a clipped region, such as Halo's panel, where the region would cut the shadow at its edges. A horizontal strip of cards pads its content so the shadow fits inside the scroll view.

TokenUse
--shadow-smA card on the ground — the resting surface
--shadow-lgFloating chrome: the tab bar, popovers, the Ask bar
--shadow-xlA sheet rising over content
--overlayThe wash behind sheets/dialogs — warm ink by Day, deeper by Night; never pure black

Motion

Purposeful and quiet. The living quality comes from one shared rhythm, not from movement everywhere.

TokenValueUse
--duration-quick120msA press, a toggle, a chip selecting
--duration-standard220msA card or row changing state; content swapping in place
--duration-gentle320msSheets rising, screens entering
--vital-tempo1.25sThe heartbeat loader and the breathing mark
--ease-standardcubic-bezier(0.2, 0, 0, 1)Things that arrive and settle
--ease-exitcubic-bezier(0.3, 0, 1, 1)Things that leave

Under reduced motion, movement becomes an opacity fade of --duration-quick or less, and loops stop.

All of the numeric scales above are mirrored as data in the package (radius, spacing, sizes, layers, icons from lumen) and machine-checked against the CSS by the truth script — drift fails the build.

On this page