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.
.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-startrather thanmargin-toprespects 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.
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).
.stack {
display: flex;
flex-direction: column;
gap: 1.5rem;
}Owl selector ( |
| |
|---|---|---|
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.
<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>.stack {
display: flex;
flex-direction: column;
gap: var(--stack-gap, 1rem);
}A
--stack-gapcustom 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.
.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 */
}: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: blocksemantics elsewhere and cannot become a flex/grid container without side effects.
gap first. It is simpler, immune to margin-collapse surprises, and the current default recommendation.Related pages: gap (row-gap & column-gap), Margin Collapse, The Cluster Layout, and CSS Custom Properties.