Stack
Vertical flow with consistent spacing, ideal for forms, card content, and content blocks.
Account
Manage profile and billing settings.
<section class="stack">
<h2>Account</h2>
<p class="muted">Manage profile and billing settings.</p>
<form>
<label class="field">
<span class="field-label">Email</span>
<input class="input" type="email" placeholder="you@example.com" />
</label>
</form>
</section>
Use stack for forms, card content, modal bodies, side panels, and documentation blocks.
Like .cluster, stack owns its children's block-axis spacing: direct-child
block margins are reset, so vertical rhythm always comes from --gap. Inline
margins stay free — for example margin-inline: auto on a child centers it
without fighting the primitive.
Don't wrap full-width regions with .stack
.stack is a column flex container, so a .center child — which sets its own
inline-size and margin-inline: auto — becomes a flex item that will not
stretch across the full row; it sizes to its content measure and centers inside
the flex line. For page-level full-width regions that each constrain their own
content, keep them in normal block flow instead:
<header class="center">…</header>
<main class="center">…</main>
<footer class="center">…</footer>
Use .stack for the vertical rhythm between related items inside one region,
not as the wrapper that lays out independent full-width regions of a page.
Choosing a rhythm
--gap is the primitive's channel. .tight and .loose set it and nothing
else; apply one on the stack to space one instance, or on an ancestor to reach
nested layouts. Pick the one that matches how tightly the children belong
together:
| Markup | Gap | Use for |
|---|---|---|
.stack gap-none | 0 | Lines forming one typographic unit |
.stack tight | 8px | A homogeneous series of controls |
.stack | 12px | Normal flow between distinct elements |
.stack loose | 24px | Sections, or blocks that read as separated |
Size changes a component's scale. Density (.compact, .spacious)
changes UI geometry and rhythm together: gap, padding, control size. Rhythm
(.tight, .loose) changes the gap only — closer or farther items, tighter
spacing without smaller buttons. Use one rhythm class per element.
One typographic unit means a name and a job title, or a figure and its caption. A homogeneous series means a radio list, a checkbox list, or a compact vertical menu.
The distinction between the first two matters. 42 above Open issues is
almost a single block of text, so it takes gap-none. Three .choice labels
stay three separate controls that happen to form one group, so they take
tight — tighter than the default, but still spaced.
.tight and .loose rebind --gap to --space-20 and --space-50, the same
steps .compact and .spacious use. Because the token is inherited, they also
tighten the inner gap of any nested stack, cluster, and grid. For a one-off gap
that must not reach nested layouts, set the gap property directly on the
instance (the next section explains why).
A stack that mixes kinds keeps the default: five .field wrappers and one lone
.choice is a form that contains a checkbox, not a list of options, and
tightening it would squeeze the fields.
Region rhythm goes on gap, not on --gap
A container that composes other primitives owns its rhythm through the gap
property. Setting --gap on it instead re-spaces everything it contains, at any
depth:
/* Leaks: --gap is inherited, so every nested stack, cluster and grid — down to
a radio list inside a field-group — is pushed to 24px too. */
.app-region {
--gap: var(--space-50);
}
/* Owns its own rhythm and nothing else. Nested primitives keep --gap. */
.app-region {
gap: var(--space-50);
}
Both give the region the same spacing; only the second stops there. This is what
makes a nested .stack tight a real rhythm choice — 12px down to 8px — rather
than a patch cancelling a rhythm inherited from three levels up.
Setting --gap is still the right move on the element that consumes it: a
one-off --gap on a card body or an actions row tunes that primitive and
nothing below it, because the setter is the consumer. The leak only appears when
a container sets the token and its children read it.
The rhythm and density classes are the deliberate exception: they set --gap
precisely so the context reaches nested layouts.
Grouping items tightly
Stack spacing is a --gap between every direct child; the primitive cannot
tighten two specific children by itself. To group a few items into one unit
(a title and its description), nest a .stack gap-none:
<div class="stack">
<div class="stack gap-none">
<strong>Ada Meridian</strong>
<span class="muted">Senior designer working on design systems.</span>
</div>
<span class="badge success soft">Active</span>
</div>
The outer stack keeps its rhythm between groups; the inner gap-none unit owns
the tight pairing.
For a one-off adjacency — pull a single child closer to its neighbour without
adding a wrapper — the escape hatch is app CSS. gap and margin-block are
additive, so a negative block margin on the child cancels the following gap:
.stack > .profile-row {
margin-block-end: calc(var(--gap) * -1);
}
The stack resets child block margins to zero, so this re-adds a margin on that one child only; the rest of the stack keeps its rhythm.