Stack

.stack is a flex column with a gap. That is the whole component — five declarations — and it is the most useful class in Deck, because it is the reason almost nothing else needs a margin.

When to use it

Use it whenever two or more things sit one above another and need space between them. That is most of a page.

Three unrelated elements, evenly spaced, no margins anywhere

Export finished

2,481 rows written to invoices-2026-03.csv.

<div class="stack"> <h3>Export finished</h3> <p>2,481 rows written to invoices-2026-03.csv.</p> <button class="btn btn-primary">Download</button> </div>
<div class="stack">
  <h3>Export finished</h3>
  <p>2,481 rows written to invoices-2026-03.csv.</p>
  <button class="btn btn-primary">Download</button>
</div>

Why gap instead of margins

This is the argument the rest of Deck rests on, so it is worth making properly. Spacing something with margins means writing a rule for every element that might sit next to every other element. The usual approach is the lobotomised owl — * + * { margin-block-start: 1rem } — and it works, but it has four problems that a gap does not.

Margins collapse; gaps do not

Adjacent vertical margins merge into the larger of the two, so a 1rem margin below one element and a 2rem margin above the next produces 2rem of space, not 3rem. Sometimes that is what you wanted. The problem is that you cannot tell by reading the markup, and the answer changes when a parent gains overflow, display: flow-root or a border.

A gap is the distance between two items. It is that distance in every circumstance.

Margins need exceptions; gaps have none

A margin below every child puts a margin below the last child too, which is why margin-based systems are full of :last-child { margin-block-end: 0 }. A gap only exists between two items, so the first and last edges are clean without an exception.

That difference shows up the moment something is conditional. Hide the last element and a margin system leaves a hole; a gap closes up.

The card on the right has no trailing space to trim
Three items

First

Second

Third

Last one hidden

First

Second

<div class="split"> <div class="card"><div class="card-body"><div class="stack stack-2"> <span class="badge">Three items</span> <p>First</p><p>Second</p><p>Third</p> </div></div></div> <div class="card"><div class="card-body"><div class="stack stack-2"> <span class="badge">Last one hidden</span> <p>First</p><p>Second</p><p hidden>Third</p> </div></div></div> </div>
<div class="split">
  <div class="card"><div class="card-body"><div class="stack stack-2">
    <span class="badge">Three items</span>
    <p>First</p><p>Second</p><p>Third</p>
  </div></div></div>
  <div class="card"><div class="card-body"><div class="stack stack-2">
    <span class="badge">Last one hidden</span>
    <p>First</p><p>Second</p><p hidden>Third</p>
  </div></div></div>
</div>

The parent owns the spacing, not the child

With margins, a heading carries its spacing wherever it goes — so the same <h3> is wrong in a card, wrong in a modal, and right only where the margin was chosen. Moving it means overriding it.

With a gap, the container decides. The same heading is correct in all three places because it brings no opinion with it. That is why Deck's components almost never set a margin: .card-body, .field, .list-main and .alert are all flex columns with a gap, and every one of them is a .stack with a different default.

A gap is direction-agnostic

margin-block-start is fine, but the moment a stack becomes a row — or the writing mode changes — the property is wrong and has to be swapped. gap means "between items" in whatever direction the container is running, so .stack and .cluster take the same --gap and neither needs an RTL rule.

The honest counter-argument: gap needs a flex or grid container, so it cannot space arbitrary elements in normal flow. Deck accepts that and uses a container almost everywhere. Where you genuinely have flow content you do not control — rendered Markdown, a CMS body — use .prose, which is the one place Deck does set margins on descendants.

The spacing scale

Every spacing utility in Deck uses the same seven steps. A step that exists in one family exists in all of them, so .stack-3, .gap-3, .p-3 and .mt-3 are all the same distance.

The seven spacing steps
Class Token Value What it is for
.stack-0 --space-0 0 Nothing. Two elements that must touch — a table inside its wrapper.
.stack-1 --space-1 .25rem Hairline. A label and the control it names, when they read as one thing.
.stack-2 --space-2 .5rem Tight. A title and its subtitle.
.stack-3 --space-3 .75rem Related blocks inside a card or a section.
.stack-4 --space-4 1rem The default. What you get with no step class at all.
.stack-6 --space-6 1.5rem Loose. Separating parts of a page.
.stack-8 --space-8 2rem Very loose. Whole sections, when .section is too much.
The same three bars at three steps
stack-1
stack-3
stack-6
<div class="grid grid-tight"> <div class="card"><div class="card-body"> <div class="stack stack-1"><span class="badge">stack-1</span><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div></div> </div></div> <div class="card"><div class="card-body"> <div class="stack stack-3"><span class="badge">stack-3</span><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div></div> </div></div> <div class="card"><div class="card-body"> <div class="stack stack-6"><span class="badge">stack-6</span><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div></div> </div></div> </div>
<div class="grid grid-tight">
  <div class="card"><div class="card-body">
    <div class="stack stack-1"><span class="badge">stack-1</span><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div></div>
  </div></div>
  <div class="card"><div class="card-body">
    <div class="stack stack-3"><span class="badge">stack-3</span><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div></div>
  </div></div>
  <div class="card"><div class="card-body">
    <div class="stack stack-6"><span class="badge">stack-6</span><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div><div class="skeleton" style="block-size:1.5rem"></div></div>
  </div></div>
</div>

Why there is no step 5

The steps are 0, 1, 2, 3, 4, 6, 8. Five is missing on purpose. At 1.25rem it is four pixels from step 4 and four from step 6 — a difference nobody sees in a real layout, and a choice nobody can make on purpose.

A half step between two neighbours is what people reach for when they are not sure, and reaching for it is how a codebase ends up with four spacings that are all almost the same. It is also, concretely, how this repository ended up with .stack-5 in its own demo when no such class existed: the markup looked right, nothing errored, and the element silently fell back to the default. The scale is short so that the choice stays deliberate.

The --space-* tokens keep their full range, 0 through 24. Tokens are the palette a component composes from; the utilities are the smaller set you write markup with. Those are different jobs, so they are different sizes.

How one class is configurable

.stack declares gap: var(--gap, var(--space-4)), and every step class sets nothing but --gap. Three consequences follow, and they are what make the pattern worth copying:

  • The step classes are one declaration each, so the whole family costs almost nothing in the stylesheet.
  • A value the scale does not have is style="--gap:3.5rem", with no new class and no !important.
  • --gap is shared with .cluster, .grid, .split, .scroller and .center — so .stack-6 on a .grid works, because both read the same property.
  • A step class on its own does nothing. It sets a property and nothing reads it, so class="stack-3" without .stack renders as an ordinary block with no gap at all — and fails silently, because the class is real. Write both: class="stack stack-3". The same goes for .card-body, which sets a fixed gap of its own that a step class cannot change; nest a .stack inside it instead, as the example below does.
A gap that is not on the scale, without a new class
<div class="stack" style="--gap:3.5rem"> <div class="skeleton" style="block-size:1.5rem"></div> <div class="skeleton" style="block-size:1.5rem"></div> </div>
<div class="stack" style="--gap:3.5rem">
  <div class="skeleton" style="block-size:1.5rem"></div>
  <div class="skeleton" style="block-size:1.5rem"></div>
</div>

Because the step classes only set a custom property, .stack-2 on a .cluster does exactly what .cluster-tight does. That is convenient and slightly untidy — see cluster, where the two naming schemes meet.

Centred columns

.center is .stack with align-items: center and text-align: center. It reads the same --gap, so the step classes work on it unchanged.

An empty state — the usual reason to reach for .center
SR

No projects yet

Create one to get started.

<div class="center stack-3"> <span class="avatar avatar-lg">SR</span> <h3>No projects yet</h3> <p class="text-muted">Create one to get started.</p> <button class="btn btn-primary">New project</button> </div>
<div class="center stack-3">
  <span class="avatar avatar-lg">SR</span>
  <h3>No projects yet</h3>
  <p class="text-muted">Create one to get started.</p>
  <button class="btn btn-primary">New project</button>
</div>

align-items: center also stops children from stretching to the full width, which is why the button above is its natural size rather than full width. In a plain .stack it would stretch.

Classes

Generated from src/04-layout.css by tools/docs/extract.mjs.

Classes, generated from the stylesheet source
Class Layer What it does Source
.stack deck.layout Vertical rhythm without margin collapse surprises 04-layout.css:51
.stack-0 deck.layout No gap. For two elements that must touch. 04-layout.css:57
.stack-1 deck.layout A hairline. A label and the control it names, when the two read as one thing. 04-layout.css:60
.stack-2 deck.layout Tight vertical rhythm, for a label and its control or a title and its subtitle. 04-layout.css:63
.stack-3 deck.layout The default rhythm for related blocks inside a card or a section. 04-layout.css:65
.stack-4 deck.layout The default, named. Same as .stack with no step class. 04-layout.css:67
.stack-6 deck.layout Loose vertical rhythm, for separating whole sections of a page. 04-layout.css:69
.stack-8 deck.layout Very loose, for whole sections of a page where .section is too much. 04-layout.css:71
.center deck.layout Center a thing with a max width and no side effects 04-layout.css:150

Tokens

The seven steps the utilities use, plus the two computed spacings that the page-level classes read.

Design tokens this component reads
Token Live Declared value
--space-0 0
--space-1 .25rem
--space-2 .5rem
--space-3 .75rem
--space-4 1rem
--space-6 1.5rem
--space-8 2rem
--space-section clamp(2.5rem, 1.4rem + 5.5vw, 6rem)
--space-gutter clamp(1rem, .55rem + 2.2vw, 2rem)

Accessibility

  • A stack has no semantics. It is a <div> with display: flex and no role. Put it on a <ul>, <section> or <form> when the grouping means something; the styles do not care what element they are on.
  • Flex changes nothing about reading order as long as you do not reorder. flex-direction: column keeps DOM order, so what a screen reader hears matches what a sighted reader sees.
  • Never use order or row-reverse to fix a layout. Both leave the DOM order — which is the tab order and the reading order — pointing one way while the visual order points the other. If the order is wrong, the markup is wrong.
  • .stack on a <ul> can silence the list. Some Safari and VoiceOver combinations drop list semantics when display: flex is applied. Add role="list" back if the item count matters.
  • Spacing is not a separator. A large gap groups things visually and says nothing to a screen reader. If two groups are genuinely distinct, use a heading or a <section>, not .stack-8.
  • Gap does not scale with text size unless the token does. Deck's steps are in rem, so they grow with the reader's root font size — a stack set in pixels would not, and a page zoomed to 200% would have text touching.

Right to left

Nothing to do. gap is not a directional property, and a column stack has no start or end edge to mirror. This is one of the clearest cases for gap over margin: margin-block-start would need no RTL rule either, but margin-left on a row would, and a gap never gives you the chance to write the wrong one.

Reduced motion

Nothing about a stack animates or transitions, so prefers-reduced-motion changes nothing here.

Printing

src/99-print.css has no rule for .stack, which is deliberate — a flex column with a gap prints correctly as it is. The layout classes that are unwound for print are .grid and .split, which become display: block so that content can break across pages instead of being trapped in a fixed track.

Overriding it

/* One stack, no layer needed */
<div class="stack" style="--gap:3.5rem">

@layer app.components {
  /* A step the scale does not have, if you need it repeatedly */
  .stack-12 { --gap: var(--space-12); }

  /* A different default for every stack on the site */
  .stack { gap: var(--gap, var(--space-3)); }
}

Prefer setting --gap over setting gap: the custom property is what every step class writes to, so a --gap override composes with them and a flat gap override silently beats them.

When not to use it

  • Not for a row. Use .cluster, which wraps, or .bar, which does not. A .stack with flex-direction: row overridden is a cluster you have to maintain.
  • Not for content you did not write. Rendered Markdown, a CMS body or a WYSIWYG field arrives as flow content with its own margins. Use .prose, which sets the vertical rhythm on descendants — the one place Deck accepts margins, because there is no container to put a gap on.
  • Not for a page's outer rhythm. Use .section, whose padding is a clamp() that grows with the viewport. A fixed .stack-8 between page sections is too much on a phone and too little on a monitor.
  • Not when the items should share a row when there is room. That is .grid, which reflows on its own width. A stack is always one column.
  • Not to fake a separator. If the reader needs to know two groups are different, give them a heading or an <hr>. Extra space is invisible to anyone not looking at the screen.