Spacing
Spacing is one scale, space.0 through space.24, built on a 4px grid. Padding, margin, and gaps all draw from it. A shared scale is what keeps a button’s inset, a card’s padding, and the gap between them visually related instead of arbitrary.
The scale
Sixteen steps. The number is the token key, not the pixel value — space.4 is 16px, not 4px. Steps 1–12 are every 4px; above that they thin out to 14, 16, 20, 24 since layout gaps rarely need 4px precision at that size.
| Token | Value | Typical use |
|---|---|---|
--helix-ui-space-0 | 0 | Reset a gap to nothing |
--helix-ui-space-1 | 4px | Hairline gap, icon-to-label |
--helix-ui-space-2 | 8px | Tight inline spacing |
--helix-ui-space-3 | 12px | Compact padding |
--helix-ui-space-4 | 16px | Default body spacing |
--helix-ui-space-5 | 20px | |
--helix-ui-space-6 | 24px | Section spacing |
--helix-ui-space-7 | 28px | Panel breathing room |
--helix-ui-space-8 | 32px | |
--helix-ui-space-9 | 36px | Marketing-block padding |
--helix-ui-space-10 | 40px | |
--helix-ui-space-11 | 44px | Touch-target floor (iOS / Android) |
--helix-ui-space-12 | 48px | |
--helix-ui-space-14 | 56px | Generous hero padding |
--helix-ui-space-16 | 64px | |
--helix-ui-space-20 | 80px | |
--helix-ui-space-24 | 96px | Top-level section gutter |
space.4 (16px) is the anchor — the default gap between unrelated body elements. space.2 (8px) binds things that belong together (an icon and its label). space.6/space.8 separate sections. space.11 (44px) is the smallest a tap target should get.
On viewports ≤767px the static build scales the whole run down about 10% through a @media (max-width: 767px) override — space.4 becomes 14px, space.24 becomes 86px. Nothing in your markup changes; the token carries the shift.
Density: the scale can breathe
The DNA density gene multiplies every --helix-ui-space-* token at runtime. This is the one knob that retunes spacing globally without touching a component.
| Allele | Multiplier | space.4 becomes | Feel |
|---|---|---|---|
compact | 0.78 | 12.48px | Dashboards, data-dense apps |
comfortable | 1.0 | 16px | Wildtype — the default |
spacious | 1.25 | 20px | Marketing, reading surfaces |
express(dna) computes each token as base × density.spacing and writes the results onto the DNA provider’s wrapper div:
cssVars[`--helix-ui-space-${key}`] = `${(base * density.spacing).toFixed(2)}px`;// compact → '--helix-ui-space-4': '12.48px'Because components reference var(--helix-ui-space-4) and never the literal 16px, a density change cascades everywhere at once — no re-render, no recomputation. A second gene, paddingScale, tunes component padding on its own channel (--helix-ui-padding-scale, wildtype 1.0) so you can pad controls more generously without also widening top-level layout gaps.
Box and Stack: numbers are tokens
You rarely write var(--helix-ui-space-*) by hand — Box and Stack take a number and resolve it to the token for you. The prop 4 becomes var(--helix-ui-space-4), so your layout inherits density and the mobile override for free.
import { Box, Stack } from '@helix-ui/core';
// Box: padding / margin props (p, px, py, m, mx, my)<Box p={6}>…</Box> {/* padding: var(--helix-ui-space-6) → 24px */}<Box px={4} py={3}>…</Box> {/* 16px inline, 12px block */}
// Stack: flex layout with a token gap (default gap={4})<Stack gap={2}> {/* column, 8px between children */} <Text>Line one</Text> <Text>Line two</Text></Stack><Stack direction="row" gap={3} align="center" justify="between"> <Text weight="semibold">Total</Text> <Text>$48.00</Text></Stack>The resolver is one rule: an all-digits value maps to var(--helix-ui-space-N); anything else ("2rem", "clamp(…)") passes through untouched, so you keep an escape hatch for the rare custom value.
if (/^\d+$/.test(s)) return `var(--helix-ui-space-${s})`;return s; // strings pass throughLayout rhythm
A few conventions keep spacing coherent across a screen:
- Pick from the scale, always.
space.5(20px) exists so you don’t reach for a bespoke 18px. If two values look interchangeable, use the same token. - Proximity encodes relationship. Tighter gaps (
space.1–space.2) mean “these belong together”; wider gaps (space.6+) mean “new group.” Spacing is information, not just air. - Nest by stepping down. A section padded at
space.8holds cards padded atspace.6holding rows gapped atspace.3. Each level insets less than its parent. - One gap per axis. Prefer a single
Stack gapover per-child margins — it collapses cleanly and there’s one number to change. - Respect the 44px floor. Interactive targets get at least
space.11of height or hit-area, even when the visual is smaller.
In practice
/* CSS — a settings row */.settings-row { display: flex; gap: var(--helix-ui-space-3); /* 12px between control and label */ padding: var(--helix-ui-space-4) var(--helix-ui-space-6); /* 16 / 24 */}// React — the same row, density-aware<Stack direction="row" gap={3} align="center"> <Switch /> <Text>Email notifications</Text></Stack>Both track density: wrap either in a spacious DNA and the 12px becomes 15px without an edit. The DTCG source, build outputs, and naming grammar for space tokens are in the Tokens reference.