Stop writing margins
You are about to put margin-bottom on something. This page is an argument
that you should not, and that the eight classes below will do the same job with fewer
bugs in it. It is the shortest route to using Deck well rather than using Deck like
Bootstrap.
The move
Spacing is a property of a group, not of the things in it. So it goes on the
parent, once, as a gap — not on each child as a margin.
The habit
<div>
<h2 class="mb-2">Title</h2>
<p class="mb-4">Body copy.</p>
<button class="btn">Action</button>
</div>
The move
<div class="stack stack-3">
<h2>Title</h2>
<p>Body copy.</p>
<button class="btn">Action</button>
</div>
One class instead of two, and the spacing is stated once instead of twice. That is the smaller half of the win. The larger half is the four bugs that are now impossible.
Four bugs you just deleted
-
The trailing space.
.mb-4on every child puts a margin under the last one too, so the group ends with a gap that is not between anything. In a card, that is 1rem of dead space above the border that nobody asked for. The usual fix is a:last-childreset — a rule you now have to write, name and remember in every component that stacks. -
Collapsing. Adjacent block margins merge into the larger of the two,
so
.mb-4above and.mt-2below give you 1rem rather than 1.5rem. That is correct CSS and it is almost never what anyone meant. It also stops collapsing the moment a parent getsdisplay: flexor a border, so the same markup spaces differently depending on a property set somewhere else entirely.gaphas no collapsing rules at all. -
Spacing that travels with the component. A margin on a child is a
claim about its surroundings, made from inside it. Put that component somewhere else
and it brings the claim along, so the same card has different spacing in a grid than
in a sidebar and you fix it with an override. A
gapis set by whatever is doing the arranging, which is the only thing that actually knows. -
Reordering. Wrap a row, reverse it, hide a child conditionally,
insert one from a template. With gaps the spacing stays right because there is one
value and the layout applies it. With margins, every one of those changes needs the
spacing re-reasoned — and a conditionally hidden child leaves its margin behind
unless you remembered
:last-childwas now a different element.
Point four is the one that bites in production rather than in review. An empty state that renders no rows, a permissions check that hides the last button, a list whose final item is filtered out — all of them silently leave a margin behind, and none of them show up until the data does.
Deck barely uses margin either
This is not advice the framework gives and does not follow. The reset opens with
* { margin: 0 } — every margin in the document is gone before anything
else happens, so a margin exists only where something deliberately adds one back.
Across 26 stylesheets and 1,766 rules there are 114 margin declarations. 34 of them are
the opt-in .mt-* and .mis-* utilities, 21 are
auto for centring or pushing, and most of the rest are
margin: 0 resets or one- and two-pixel optical nudges. The number that are
actually spacing one thing away from another is 29, and the majority of
those are the heading rules in 03-type.css — because a heading's space
above it genuinely does belong to the heading.
Everything else is a gap. The whole component library is built the way this page is asking you to build.
The eight classes
You lay out with about eight classes, not forty. Each one is a container, each one sets a gap, and each one is documented in full on its own page.
| Class | Is | Reach for it when |
|---|---|---|
.stack
|
A column with a gap | Anything stacked vertically. The default. |
.cluster
|
A wrapping row with a gap | Buttons, tags, anything horizontal that must survive narrow screens. |
.grid
|
Auto-fitting columns | Cards. Chooses its own column count from a minimum width. |
.split
|
Two columns, one on a phone | A sidebar and content, a form and a preview. |
.bar
|
A row that does not wrap | Toolbars and headers, where wrapping would be wrong. |
.center
no page yet
|
A centred column | A hero, an empty state, a sign-in box. |
.container
|
Centred, capped, gutter applied | The page shell. Almost every page starts with one. |
.section
|
Vertical page rhythm | The band of air between major sections. |
Changing the gap
Every primitive reads the same custom property, so the size variants are all the same seven steps and they behave identically across the eight classes.
.stack { display: flex; flex-direction: column; gap: var(--gap, var(--space-4)); }
.stack-2 { --gap: var(--space-2); }
Which means two ways to change it, and both are legitimate. Use the named step when it exists, and set the property directly when you want a value the scale does not carry:
<div class="stack stack-2">…</div> <!-- named step -->
<div class="stack" style="--gap: var(--space-10)">…</div> <!-- any token -->
<div class="stack stack-1 bg-sunken p-3 r-md">
<div class="bg-surface p-2 r-sm">stack-1</div>
<div class="bg-surface p-2 r-sm">4px apart</div>
</div>
<div class="stack stack-4 bg-sunken p-3 r-md">
<div class="bg-surface p-2 r-sm">stack-4</div>
<div class="bg-surface p-2 r-sm">16px apart</div>
</div>
<div class="stack stack-8 bg-sunken p-3 r-md">
<div class="bg-surface p-2 r-sm">stack-8</div>
<div class="bg-surface p-2 r-sm">32px apart</div>
</div>
<div class="stack stack-1 bg-sunken p-3 r-md">
<div class="bg-surface p-2 r-sm">stack-1</div>
<div class="bg-surface p-2 r-sm">4px apart</div>
</div>
<div class="stack stack-4 bg-sunken p-3 r-md">
<div class="bg-surface p-2 r-sm">stack-4</div>
<div class="bg-surface p-2 r-sm">16px apart</div>
</div>
<div class="stack stack-8 bg-sunken p-3 r-md">
<div class="bg-surface p-2 r-sm">stack-8</div>
<div class="bg-surface p-2 r-sm">32px apart</div>
</div>
Composing them
The primitives nest, and nesting is how you get real layouts rather than by adding more classes. A page is usually a container, a section, a split, and stacks inside that.
<main class="container section">
<div class="stack stack-8">
<div class="stack stack-2">
<h1>Invoices</h1>
<p class="lede">Everything issued in the last 90 days.</p>
</div>
<div class="bar">
<input class="input" type="search" placeholder="Search">
<button class="btn btn-primary push">New invoice</button>
</div>
<div class="grid">
<article class="card"><div class="card-body">…</div></article>
<article class="card"><div class="card-body">…</div></article>
</div>
</div>
</main>
Invoices
Everything issued in the last 90 days.
INV-2290
Pending<div class="stack stack-6">
<div class="stack stack-2">
<h3>Invoices</h3>
<p class="lede">Everything issued in the last 90 days.</p>
</div>
<div class="bar">
<input class="input" type="search" placeholder="Search">
<button class="btn btn-primary push">New invoice</button>
</div>
<div class="grid">
<article class="card"><div class="card-body"><h4 class="card-title">INV-2291</h4><span class="badge badge-good">Paid</span></div></article>
<article class="card"><div class="card-body"><h4 class="card-title">INV-2290</h4><span class="badge badge-warn">Pending</span></div></article>
</div>
</div>
<div class="stack stack-6">
<div class="stack stack-2">
<h3>Invoices</h3>
<p class="lede">Everything issued in the last 90 days.</p>
</div>
<div class="bar">
<input class="input" type="search" placeholder="Search">
<button class="btn btn-primary push">New invoice</button>
</div>
<div class="grid">
<article class="card"><div class="card-body"><h4 class="card-title">INV-2291</h4><span class="badge badge-good">Paid</span></div></article>
<article class="card"><div class="card-body"><h4 class="card-title">INV-2290</h4><span class="badge badge-warn">Pending</span></div></article>
</div>
</div>
Count the responsive code in that: none. .grid chooses its own column
count from a minimum column width, so it is three across on a monitor and one on a
phone without a breakpoint being named. .bar keeps the toolbar on one
line. .container applies a gutter that is fluid between 1rem and 2rem.
Where a margin is still right
This is not a prohibition. Three cases where a margin is the correct tool and a gap is not:
-
One element, one neighbour. A back link that needs to sit further
from the heading below it than the stack's rhythm allows. That is genuinely about
those two elements and not about a group.
.mt-6and move on. -
Centring and pushing.
.mx-autocentres a block;.mis-autopushes an item to the end of a flex row. These areautomargins, which are a layout mechanism rather than spacing, and there is no gap equivalent. -
Space above a heading in flowing prose. Deck does this itself. In a
long article the gap before an
<h2>should be larger than the gap after it, and a singlegapcannot express an asymmetry.
The tell for a wrong margin is whether removing it from one element would make the layout wrong for its siblings. If yes, it was the container's job.
Two things that will look broken
.gap-3on a plain<div>does nothing-
gaponly applies to flex, grid and multi-column containers. On anything else it is inert, silently. Check the parent is really a flex or grid container — usually you wanted.stack, which makes it one, with a step such as.stack-3beside it for the gap. The step alone is the same trap: it sets a gap property and nothing reads it. - A long word or a wide table pushes the page sideways
-
A flex item will not shrink below its content, and a grid track's minimum is
auto. So one unbreakable string inside a flex row drags the whole layout past the viewport and you get a horizontal scrollbar on the body. The fix is.grow— which setsmin-inline-size: 0along with the flex — or.min-is-0on a grid child. This is the most common layout bug in any flex-based system and it is worth recognising on sight.
Next
The spacing reference has the scale itself and why the utilities stop at seven steps. Each primitive has a page of its own — stack, cluster, grid and split are the four you will use most, and reading those four is about ten minutes well spent.