CSSThe Stack Layout

The Stack Layout

The Stack is the simplest of the Every Layout patterns and arguably the most-used: vertical rhythm between a run of elements — paragraphs in an article, form fields, cards in a list — with one consistent space between every sibling. It sounds trivial until you notice how much CSS actually goes into getting margins right by hand, element by element.

The owl selector — the classic approach

Heydon Pickering's "owl selector," so named because * + * looks a little like an owl's face, applies a top margin to every element that has a preceding sibling — meaning the first child never gets an unwanted margin above it.

CSS
.stack > * + * {
  margin-block-start: 1.5rem;
}
  • > * selects every direct child, regardless of tag — headings, paragraphs, lists, images, all get the same treatment.

  • + * (the adjacent sibling combinator, see Combinators) only matches an element that has an immediately preceding sibling — so the very first child, having none, is skipped automatically.

  • margin-block-start rather than margin-top respects the writing mode (see logical properties) — the space is always "before" the element in reading order, even in vertical or right-to-left writing modes.

Note
This is functionally the same idea as flexbox's gap (see the gap (row-gap & column-gap) page), but the owl selector predates wide gap support and still matters for one reason: it works on any display type, including plain block-flow content with no flex/grid container at all.
Gap-based stacks (the modern default)

Where the container can be a flex or grid column, gap does the same job with less selector cleverness and no risk of the space "leaking" if a child is later removed via JavaScript (margins on now-first children don't need re-checking; gap just works between whatever is present).

CSS
.stack {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

Owl selector (* + * margin)

gap

Works on plain block content

Yes

No — needs flex/grid

Handles dynamically added/removed children

Automatically

Automatically

Space collapses with child margins

Can interact with margin collapse

Never — gap is immune to margin collapse

Browser support needed

Universal

Flexbox/Grid gap — universal since ~2020

Nested stacks

A form is often a stack of fields, where each field is itself a small stack of a label and an input. Nesting the pattern — one .stack inside another — keeps every level's rhythm independent and explicit rather than fighting a single global margin rule.

HTML
<form class="stack" style="--stack-gap: 1.5rem">
  <div class="stack" style="--stack-gap: 0.5rem">
    <label for="name">Name</label>
    <input id="name" />
  </div>
  <div class="stack" style="--stack-gap: 0.5rem">
    <label for="email">Email</label>
    <input id="email" type="email" />
  </div>
  <button type="submit">Submit</button>
</form>

CSS
.stack {
  display: flex;
  flex-direction: column;
  gap: var(--stack-gap, 1rem);
}
  • A --stack-gap custom property (see Custom Properties) lets every nesting level set its own rhythm without a different class per level.

  • The outer stack (form fields, 1.5rem apart) reads visually distinct from the inner stack (label-to-input, a tighter 0.5rem) purely from spacing — no extra dividers needed.

Recursive spacing — a scale, not one fixed value

A single hardcoded gap works for a simple list, but richer content (an article with headings, paragraphs, and blockquotes mixed together) usually wants more space before a heading than between two paragraphs. Recursive/exception-based spacing solves this without breaking the "one rule governs everything" simplicity.

CSS
.stack > * + * {
  margin-block-start: 1rem; /* the base rhythm */
}

.stack > * + :is(h2, h3) {
  margin-block-start: 2.5rem; /* headings get extra breathing room */
}

.stack > :is(h2, h3) + * {
  margin-block-start: 0.75rem; /* but tighter to whatever follows them */
}
Tip
Layer exceptions with :is() (see the :not(), :is() & :where() page) rather than writing one bespoke selector per heading level — it keeps the exception list short and readable as the content model grows.
When margin beats gap
  • You don't control the markup enough to wrap the stacked items in a flex/grid parent (e.g. rendering raw markdown/CMS HTML straight into a container) — the owl selector applies to arbitrary children without needing to touch the parent's display mode.

  • You need an exception-based scale like the heading example above, where different sibling pairs need different spacing based on what they are, not just their position — margin-based selectors give you that per-pair control naturally.

  • You are retrofitting spacing onto legacy block content that already relies on display: block semantics elsewhere and cannot become a flex/grid container without side effects.

Note
For everything else — a fresh component, a card list, a form — reach for gap first. It is simpler, immune to margin-collapse surprises, and the current default recommendation.