Elevation, radius & motion
Three smaller systems share a page because they share a job: giving flat rectangles shape, depth, and behavior. Each is a short scale of tokens, and each moves under a DNA gene.
Radius
Six steps, from square to pill. The named steps map to component classes of increasing size.
| Token | Value | Use for |
|---|---|---|
--helix-ui-radius-none | 0 | Sharp corner, flush edges |
--helix-ui-radius-sm | 4px | Chips, badges, tags |
--helix-ui-radius-md | 8px | Buttons, inputs, selects |
--helix-ui-radius-lg | 12px | Cards, panels |
--helix-ui-radius-xl | 16px | Modals, sheets |
--helix-ui-radius-full | 9999px | Pills, avatars, toggles |
The step tracks the element’s size: a badge at sm and a modal at xl read as the same corner softness because the radius grows with the box. full is a sentinel — 9999px clamps to a semicircle on any height, so a pill stays a pill whether it’s 24px or 40px tall.
On viewports ≤767px the static build eases the larger radii ~10% (md 8→7px, lg 12→11px, xl 16→14px) so corners don’t look chunky on small screens.
Radius moves under DNA
The radius gene multiplies every corner by a factor; the wildtype is standard (1.0).
| Allele | Factor | radius.md becomes |
|---|---|---|
sharp | 0.35 | 2.8px |
subtle | 0.7 | 5.6px |
standard | 1.0 | 8px |
rounded | 1.4 | 11.2px |
extreme | 2.2 | 17.6px |
express(dna) computes base × radius.factor × curveFactor and rewrites --helix-ui-radius-* on the DNA wrapper. A second gene, radiusCurve, bends the ramp — progressive shrinks small radii and grows large ones (chips get sharper, sheets get rounder) rather than scaling them uniformly.
Elevation (shadows)
Three levels. Depth is the only thing they encode — a bigger shadow means “floats higher above the page,” which maps to how transient the surface is.
| Token | Value | Use for |
|---|---|---|
--helix-ui-shadow-sm | 0 1px 2px 0 rgb(0 0 0 / 0.05) | Cards resting on a page |
--helix-ui-shadow-md | 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1) | Popovers, dropdowns |
--helix-ui-shadow-lg | 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1) | Modals, sheets |
Each level is two stacked shadows — a tight near-shadow plus a softer far one — which reads more like real light than a single blur. The mapping is intentional: the more temporary a surface, the higher it floats. A card (sm) is part of the page; a dropdown (md) is hovering; a modal (lg) has taken over.
Elevation moves under DNA
The surface gene swaps the entire shadow set. Wildtype is elevated, whose values are exactly the three above.
flat— near-invisible hairline shadows (smisnone,mdis a single0 1px 0 0 rgb(0 0 0 / 0.06)). For dense or print-like UI.elevated— the standard soft-drop set shown above.glassy— a top highlight plus a deep, wide shadow (0 1px 0 0 rgb(255 255 255 / 0.08), 0 8px 24px -8px rgb(0 0 0 / 0.18)atmd) for translucent/blurred surfaces.
Motion
Motion is two token groups: duration (how long) and easing (the velocity curve).
Duration
| Token | Value | Use for |
|---|---|---|
--helix-ui-motion-duration-instant | 0ms | Suppress a transition entirely |
--helix-ui-motion-duration-fast | 120ms | Hover, press, focus ring, tooltip |
--helix-ui-motion-duration-normal | 180ms | Most state changes, popovers, toasts |
--helix-ui-motion-duration-slow | 240ms | Sheets, dialogs, drawers |
--helix-ui-motion-duration-slower | 320ms | Collapsing sections, page transitions |
The rule of thumb: duration scales with travel. A focus ring barely moves, so fast; a drawer crosses the viewport, so slow. Anything past slower starts feeling broken rather than deliberate.
Easing
| Token | Value | Use for |
|---|---|---|
--helix-ui-motion-easing-linear | linear | Spinners, indeterminate progress |
--helix-ui-motion-easing-standard | cubic-bezier(0.2, 0, 0, 1) | Default UI ease |
--helix-ui-motion-easing-emphasized | cubic-bezier(0.32, 0.72, 0, 1) | Sheets, toasts |
--helix-ui-motion-easing-decelerate | cubic-bezier(0, 0, 0.2, 1) | Elements entering the viewport |
--helix-ui-motion-easing-accelerate | cubic-bezier(0.4, 0, 1, 1) | Elements leaving the viewport |
--helix-ui-motion-easing-spring | cubic-bezier(0.16, 1, 0.3, 1) | Entrance scale-ups, hover lifts |
The pairing that matters most: things entering use decelerate (fast in, gentle stop — they arrive and settle); things leaving use accelerate (slow start, quick exit — they’re gone before you track them). standard handles the in-place changes that are neither.
Shared motion utilities
@helix-ui/core ships a motion stylesheet (motion.css) of keyframes and classes so you rarely write @keyframes yourself. Each references the tokens above, so overriding a token retunes the whole system.
<div className="helix-ui-anim-slide-in-up">Appears from below</div><div className="helix-ui-anim-stagger"> {/* children with .helix-ui-anim-* enter in a 20ms → 40ms cascade */}</div><button className="helix-ui-press">Scales to 0.97 on :active</button>Motion moves under DNA
The motion gene rescales durations. Wildtype is standard; subtle compresses everything (fast 90ms / normal 150ms / slow 220ms), lively stretches it (160 / 280 / 420ms). The gene retunes --helix-ui-motion-duration-* on the wrapper, and a separate easing gene can swap the curves (e.g. a bouncier spring set) for a different feel without touching a component.
Reduced motion
Honored once, globally, at the bottom of motion.css. When the OS reports prefers-reduced-motion: reduce, every transition and animation collapses:
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; }}Durations go to 0.01ms, not 0 — that keeps them effectively instant while still firing the transitionend / animationend events that component logic listens for, so nothing hangs waiting on an animation that got skipped. Because the rule is global, individual components don’t each guard for reduced motion; they inherit it. The instant (0ms) token is the manual equivalent when you need to suppress one transition in code.
In practice
/* CSS — a card that lifts on hover */.tile { border-radius: var(--helix-ui-radius-lg); /* 12px */ box-shadow: var(--helix-ui-shadow-sm); transition: box-shadow var(--helix-ui-motion-duration-fast) var(--helix-ui-motion-easing-standard), transform var(--helix-ui-motion-duration-fast) var(--helix-ui-motion-easing-standard);}.tile:hover { box-shadow: var(--helix-ui-shadow-md); transform: translateY(-2px);}// React — components already resolve these internally<Card>Rests at radius-lg + shadow-sm</Card><Dialog>Enters at duration-slow with the decelerate ease</Dialog>Every value here is a token, so a DNA swap — sharper corners, flatter shadows, snappier motion — re-derives all of it at once. The DTCG source and build outputs for shape, elevation, and motion tokens are in the Tokens reference.