Skip to content

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.

TokenValueTypical use
--helix-ui-space-00Reset a gap to nothing
--helix-ui-space-14pxHairline gap, icon-to-label
--helix-ui-space-28pxTight inline spacing
--helix-ui-space-312pxCompact padding
--helix-ui-space-416pxDefault body spacing
--helix-ui-space-520px
--helix-ui-space-624pxSection spacing
--helix-ui-space-728pxPanel breathing room
--helix-ui-space-832px
--helix-ui-space-936pxMarketing-block padding
--helix-ui-space-1040px
--helix-ui-space-1144pxTouch-target floor (iOS / Android)
--helix-ui-space-1248px
--helix-ui-space-1456pxGenerous hero padding
--helix-ui-space-1664px
--helix-ui-space-2080px
--helix-ui-space-2496pxTop-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.

AlleleMultiplierspace.4 becomesFeel
compact0.7812.48pxDashboards, data-dense apps
comfortable1.016pxWildtype — the default
spacious1.2520pxMarketing, reading surfaces

express(dna) computes each token as base × density.spacing and writes the results onto the DNA provider’s wrapper div:

packages/dna/src/express.ts
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.

packages/core/src/utils/space.ts
if (/^\d+$/.test(s)) return `var(--helix-ui-space-${s})`;
return s; // strings pass through

Layout rhythm

A few conventions keep spacing coherent across a screen:

  1. 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.
  2. Proximity encodes relationship. Tighter gaps (space.1space.2) mean “these belong together”; wider gaps (space.6+) mean “new group.” Spacing is information, not just air.
  3. Nest by stepping down. A section padded at space.8 holds cards padded at space.6 holding rows gapped at space.3. Each level insets less than its parent.
  4. One gap per axis. Prefer a single Stack gap over per-child margins — it collapses cleanly and there’s one number to change.
  5. Respect the 44px floor. Interactive targets get at least space.11 of 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.