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

  1. The trailing space. .mb-4 on 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-child reset — a rule you now have to write, name and remember in every component that stacks.
  2. Collapsing. Adjacent block margins merge into the larger of the two, so .mb-4 above and .mt-2 below 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 gets display: flex or a border, so the same markup spaces differently depending on a property set somewhere else entirely. gap has no collapsing rules at all.
  3. 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 gap is set by whatever is doing the arranging, which is the only thing that actually knows.
  4. 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-child was 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.

The layout primitives and what each is for
ClassIsReach 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 -->
The same class, three steps. No margins involved
stack-1
4px apart
stack-4
16px apart
stack-8
32px apart
<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>
Five nested containers, zero margins, and it is already responsive

Invoices

Everything issued in the last 90 days.

INV-2291

Paid

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-6 and move on.
  • Centring and pushing. .mx-auto centres a block; .mis-auto pushes an item to the end of a flex row. These are auto margins, 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 single gap cannot 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-3 on a plain <div> does nothing
gap only 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-3 beside 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 sets min-inline-size: 0 along with the flex — or .min-is-0 on 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.