Skip to content

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.

TokenValueUse for
--helix-ui-radius-none0Sharp corner, flush edges
--helix-ui-radius-sm4pxChips, badges, tags
--helix-ui-radius-md8pxButtons, inputs, selects
--helix-ui-radius-lg12pxCards, panels
--helix-ui-radius-xl16pxModals, sheets
--helix-ui-radius-full9999pxPills, 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).

AlleleFactorradius.md becomes
sharp0.352.8px
subtle0.75.6px
standard1.08px
rounded1.411.2px
extreme2.217.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.

TokenValueUse for
--helix-ui-shadow-sm0 1px 2px 0 rgb(0 0 0 / 0.05)Cards resting on a page
--helix-ui-shadow-md0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)Popovers, dropdowns
--helix-ui-shadow-lg0 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 (sm is none, md is a single 0 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) at md) for translucent/blurred surfaces.

Motion

Motion is two token groups: duration (how long) and easing (the velocity curve).

Duration

TokenValueUse for
--helix-ui-motion-duration-instant0msSuppress a transition entirely
--helix-ui-motion-duration-fast120msHover, press, focus ring, tooltip
--helix-ui-motion-duration-normal180msMost state changes, popovers, toasts
--helix-ui-motion-duration-slow240msSheets, dialogs, drawers
--helix-ui-motion-duration-slower320msCollapsing 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

TokenValueUse for
--helix-ui-motion-easing-linearlinearSpinners, indeterminate progress
--helix-ui-motion-easing-standardcubic-bezier(0.2, 0, 0, 1)Default UI ease
--helix-ui-motion-easing-emphasizedcubic-bezier(0.32, 0.72, 0, 1)Sheets, toasts
--helix-ui-motion-easing-deceleratecubic-bezier(0, 0, 0.2, 1)Elements entering the viewport
--helix-ui-motion-easing-acceleratecubic-bezier(0.4, 0, 1, 1)Elements leaving the viewport
--helix-ui-motion-easing-springcubic-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.