skip to content

In CSS, what does the selector `.stack > * + *` match, and why is it used to space a list of children?

level: middleimportance: nice to knowfreq 36%

answer

  1. universal selector plus a sibling condition
  2. only one child lacks a predecessor
  3. space belongs between items
  4. no edge case to patch afterwards
  5. the child combinator bounds the reach

basics

~20 s

It matches every direct child of .stack that has a preceding element sibling — in other words, every child except the first. Applying margin-top there spaces the children apart without leaving a stray margin above the first one or below the last.

solid answer

~50 s

Read it right to left: the key is `*`, the universal selector, which matches any element; `+ *` says it must be immediately preceded by an element sibling; and `.stack >` restricts it to direct children of `.stack`. The net effect is "every child of `.stack` except the first", because only the first child has no preceding sibling. That is why the pattern — often called the owl — is used with `margin-block-start` to space a stack: the gap is defined *between* items rather than on items, so there is no leading margin to strip off the first child and no trailing margin under the last, and the rule keeps working when items are added, removed, or reordered. It is element-agnostic, so a stack of mixed headings, paragraphs and components all get consistent spacing from one rule.

code

html · 8 lines
html
<style>
  .stack > * + * { margin-block-start: 1rem; }
</style>
<div class="stack">
  <h2>No margin above me</h2>
  <p>Spaced from the heading</p>
  <p>Spaced from the paragraph</p>
</div>

go deeper

for a junior

Be able to say that * matches any element and that * + * means "has an element right before it", so the rule hits every child except the first.

for a middle

Explain each piece of the selector and why margin defined between items removes the need for a first-child or last-child override entirely.

for a senior

Show that you understand blast radius — why the child combinator is load-bearing here, and when gap on a flex or grid container is the more direct expression of the same intent.

for a principal

Frame it as a spacing convention decision: whether a codebase expresses rhythm through container-level gap, a shared stack primitive, or per-component margins is a consistency call with long-term maintenance cost.

## Decoding the selector `.stack > * + *` looks cryptic but is three ordinary pieces: - `*` — the universal selector, which matches an element of any type. - `+` — the next-sibling combinator: the matched element must be immediately preceded by an element sibling under the same parent. - `>` — the child combinator: the match must be a direct child of `.stack`. As always, the rightmost compound is what gets styled. So this rule styles *elements* that (a) are direct children of `.stack` and (b) have an element immediately before them. Only the first child of any parent lacks a preceding element sibling, so the match set is precisely "all children except the first". ```css .stack > * + * { margin-block-start: 1rem; } ``` ## Why "all but the first" is the useful set Spacing a vertical list of items is a between-items problem. If you put `margin-bottom` on every child you get an unwanted gap under the last one; if you put `margin-top` on every child you get an unwanted gap above the first. Both are then patched with an override for the offending edge child, which is an extra rule that has to stay in sync. Defining the space only where two items meet removes the special case entirely. Nothing needs to know how many children there are, which one is last, or what type each one is. Adding a component to the middle of the stack inherits the spacing automatically, and conditionally rendering the first item does not leave a phantom gap behind. ## Why the universal selector, specifically Using `*` rather than a type or class means the rule does not care what the children are: ```html <div class="stack"> <h2>Heading</h2> <p>Paragraph</p> <my-widget></my-widget> </div> ``` All three get consistent spacing from one declaration. The `>` matters just as much: without it, `.stack * + *` would reach into the *insides* of every child and space their internal elements too, which is almost never what you want. Restricting to direct children keeps the rule's blast radius to one level. ## What `+` counts as a sibling The next-sibling combinator considers only *element* siblings. Text nodes and comments between two elements are ignored, so whitespace and formatting in the HTML source never break the match. But a real element in between does count as a sibling and shifts what "immediately preceded" means — which is exactly what makes the pattern robust: whatever the previous element is, the current one is still spaced from it. There is one consequence worth naming: an element that is visually hidden but still in the flow of the DOM is still an element sibling, so it still participates. If a child is conditionally rendered as an empty wrapper rather than removed, the spacing chain sees it. ## Relationship to gap On a flex or grid container, the `gap` property expresses the same intent more directly and does not touch margins at all. Where the container is not a flex or grid container — plain block flow, prose, arbitrary content — the owl remains a clean way to express between-items spacing, and it is common to see both approaches in one codebase. The important thing in an interview is being able to *read* the selector and explain why the match set is "all but the first", not to argue that one spacing technique is universally correct. ## Variations you may be shown `* + *` with no ancestor scoping applies to every element on the page that has a preceding sibling — a global reset-flavoured version that is far more invasive and usually a mistake in a shared codebase. `.stack > :not(:first-child)` expresses a similar idea through a pseudo-class instead of a combinator. And swapping `+` for `~` breaks the pattern: `~` would match every following sibling relative to *each* earlier sibling, which is a different, much larger match set and not what the spacing rule wants. ## Reading it under pressure If an interviewer writes `.stack > * + *` on a whiteboard, narrate it: "any element, that immediately follows an element sibling, that is a direct child of `.stack` — so every child but the first." That sentence is the whole answer.

  • What breaks if you drop the `>` and write `.stack * + *`?
    The descendant combinator lets the rule reach every level inside the stack, so it also adds margin between elements *within* each child — paragraphs inside a card, list items inside a nested list. The child combinator confines the rule to one level, which is what makes the pattern predictable in a component-based layout.
  • How does this compare with using `gap` on a flex or grid container?
    `gap` expresses between-items spacing natively on flex and grid containers and never touches margins, so it is the more direct tool when the container is already flex or grid. The owl selector covers the same intent in plain block flow, where `gap` does not apply, and it works with arbitrary mixed content.
  • Why does swapping `+` for `~` change the result?
    `~` matches every *following* sibling, not just the immediately adjacent one, so the match set is no longer "all but the first" in a between-items sense — every later child matches relative to several earlier ones. For a spacing rule the distinction does not change which elements match here, but `~` expresses a looser relationship and is the wrong description of the intent.

saying these in an interview costs you the question

  • Saying it matches every child including the first
  • Thinking `*` alone means the container itself
  • Believing whitespace text nodes break the + match
  • Dropping the > and expecting the same reach
  • Calling it a performance optimisation rather than a spacing pattern

context