Gap, the universal spacing default
How Tailwind's gap utility became the default tool for spacing items inside flex and grid layouts, leaving margin a narrow supporting role.
You have a vertical stack of cards, or a row of buttons, and you need even space between them. It’s the most ordinary layout task there is, and the answer has changed three times in seven years. Around 2018 you put margin-bottom on every card except the last one, and that exception cost you real effort every time: a :last-child rule, or per-item bookkeeping you had to remember. Around 2021 Tailwind shipped space-y-*, which automated the bookkeeping but kept the same underlying trick. Today, in 2026, you do none of that. You put flex flex-col (or grid) on the parent and add one gap-*. The parent declares the layout, gap does the spacing, and not a single child carries a spacing rule of its own.
<ul className="flex flex-col"> {invoices.map((invoice) => ( <li key={invoice.id} className="mb-4 last:mb-0 rounded-lg border p-4" > {invoice.title} </li> ))}</ul><ul className="flex flex-col gap-4"> {invoices.map((invoice) => ( <li key={invoice.id} className="rounded-lg border p-4"> {invoice.title} </li> ))}</ul>That’s the whole shift in two snippets: the spacing rule moves off every child and onto the parent, once. You’ve been writing gap-* for two lessons already, since the flexbox and grid lessons used it as the assumed default without stopping to justify it. This lesson is where that justification finally lands. By the end you’ll reach for gap automatically inside any flex or grid container, know the one narrow case where margin still earns its place, and recognize the legacy patterns on sight when an old codebase, or an AI trained on a decade of old codebases, hands them to you.
This builds directly on the box model from The box model and the inline/block axis: you already know padding and margin, you know the --spacing scale that feeds them, and you’ve met margin collapse. This lesson slots gap between padding and margin, and in doing so it narrows margin’s job down to almost nothing.
Padding, gap, margin: one spatial model
Section titled “Padding, gap, margin: one spatial model”Start with the model rather than the utilities. Once it’s clear, “where does this spacing go?” stops being a choice among three competing options and becomes one obvious answer per situation. The three spacing tools are not interchangeable. Each one answers the same question, “where does this space sit relative to the element?”, with a different answer.
- Padding is space inside the element, between its border and its content. It pushes the element’s own content inward. You met it last as the inner band of the box model.
- Gap is space between sibling items inside a flex or grid container. It belongs to the parent, not to the children, and it sits only between items. It never adds a leading edge before the first item or a trailing edge after the last one.
- Margin is space outside the element, pushing it away from neighbors that are not its flex or grid siblings.
Gap is the one this lesson centers on, so look closely at where it lives.
The practical takeaway is this: in 2026 you write padding and gap constantly, and margin almost never. Padding handles every “space inside,” gap handles every “space between siblings,” and together those cover the overwhelming majority of real layouts. Margin is reserved for one narrow job, pushing an element away from something outside its container, plus the mx-auto centering case you already met. If you ever catch yourself writing margin between siblings inside a flex or grid container, treat it as a sign something is off. The fix is always the same: delete the margin and put gap on the parent.
Gap, the universal spacing default
Section titled “Gap, the universal spacing default”Now look at the property itself. gap is a single declaration you set on the parent of a flex, grid, or multi-column container, and it spaces all the children at once. Tailwind’s gap-* compiles straight to it: gap-4 becomes gap: calc(var(--spacing) * 4), riding the same --spacing scale every p-* and m-* uses. That shared scale matters, because it keeps your spacing between elements in lockstep with your spacing inside them. Both resolve through the one variable you can tune in @theme.
The defining behavior, the one that makes gap the right call and not just a convenient one, is that it adds space between items and nowhere else. There is no gap before the first child and none after the last. Edge spacing isn’t gap’s job; that’s what the parent’s own padding is for. So the canonical container is two utilities working as a pair:
<div className="flex flex-col gap-3 p-4"> <Row /> <Row /> <Row /></div>p-4 insets every edge of the container uniformly, and gap-3 opens an even channel between the rows. Neither one steps on the other, and no child carries a spacing rule. This separation is exactly why gap survives changes that break the legacy patterns: the spacing is computed from the layout, not pinned to individual children.
That robustness shows up most clearly when items wrap or when the count changes. Add a fifth child to a gap-3 column and it inherits the spacing for free: there’s no :last-child exception to update, no per-item margin to add. Let a row wrap to a second line and gap spaces both axes automatically, the horizontal channel between items on a row and the vertical channel between the rows themselves. Keep that wrapping behavior in mind, because it’s the first place we’ll watch the legacy approach fail in the next section.
For the rare case where you want different spacing on each axis, gap splits into two: gap-x-* controls the horizontal channel (column-gap) and gap-y-* the vertical one (row-gap). You’ll reach for these mostly in wrapping layouts where rows should sit closer together than the items within a row, or the reverse. It’s worth knowing that the plain gap-* you write every day is really CSS shorthand for row-gap plus column-gap set to the same value, though in practice you’ll write the Tailwind utility, not the longhand.
Slide the spacing yourself and watch every channel respond at once. The playground below is a small flex container of chips with two controls: a gap slider and a direction toggle between a row and a column. Drag the gap and notice that every channel updates together, with no per-child edits. Then flip the direction and confirm that the same one property spaces a column exactly as cleanly as it spaced a row.
There’s one stale worry to put down before we go further. Older tutorials warned that gap “doesn’t work in flexbox.” That was true years ago, and it’s the reason you’ll still see the legacy tricks in old code. It hasn’t been true since 2021. gap on flex containers ships in every major browser and clears the high nineties of global support, so you can use it without fallbacks and without any caveat to carry.
Why gap wins over the legacy patterns
Section titled “Why gap wins over the legacy patterns”The aim of this section is to leave you able to fix these patterns on reflex the moment one shows up in a file you’re editing. Every legacy approach shares one mechanism, and every failure below is a symptom of it, so it’s worth naming that mechanism first.
Tailwind’s space-x-* and space-y-* utilities compile to a selector that targets every child except the last one and gives it a margin. space-y-4, for instance, puts margin-bottom on each child but the final one. The exact selector is :where(& > :not(:last-child)); you’ll never type it, but its shape is what matters. The spacing is computed per child, by DOM position: “are you the last child or not?” That single fact is the seed of every break, because it makes the spacing depend on which child happens to be structurally last and on the source order of the children. gap, by contrast, is one property on the parent, with no per-child selector and no position arithmetic at all. Each failure mode below falls out of that difference.
First, wrap breakage. space-x-* puts a horizontal margin on each item except the last, and a margin knows nothing about where rows break. Let the items wrap to a second line and the spacing falls apart: items don’t line up between rows, and there is no vertical spacing between the rows at all, because the trick only ever added horizontal margins. gap handles both axes the instant items wrap, an even horizontal channel and an even vertical channel, with no extra code. This is exactly the job gap-x and gap-y unify into one property.
<div className="flex flex-wrap space-x-2"> {tags.map((tag) => ( <span key={tag} className="rounded-full border px-3 py-1"> {tag} </span> ))}</div>Breaks on wrap. The horizontal margin sits on each tag but the last; when the tags wrap to a second row they misalign, and there’s no spacing between the rows at all.
<div className="flex flex-wrap gap-2"> {tags.map((tag) => ( <span key={tag} className="rounded-full border px-3 py-1"> {tag} </span> ))}</div>Wraps cleanly. One property spaces both axes: even horizontal channels within a row and even vertical channels between rows, however the tags happen to wrap.
Second, hidden and reordered children. This is the failure that matters most for the work you’ll actually do, because it’s where the per-child mechanism turns into a real bug in a real React component. The space-y-* margins live on “every child except the last.” Now hide the last child with the hidden class, which sets display: none, the way you toggle a row off without unmounting it. That child keeps its place in the DOM as the structurally-last child, so the selector still skips it. The row that is now last on screen is no longer the structurally-last one, so the selector still hands it a bottom margin meant to sit between rows. The result is a phantom gap hanging below the visible list, spacing against nothing. Reorder children with order-* and you hit the mirror image: the child the selector skips is no longer the one that sits last on screen, so the margins land in the wrong places and the spacing comes out uneven. gap avoids both, because it spaces only the items that are actually laid out. A display: none child is pulled out of layout entirely, so no space is ever reserved for it, and there is no “last child” for the spacing to get wrong.
<ul className="space-y-4"> <li>Profile</li> <li>Billing</li> <li className="hidden">Team settings</li></ul>Phantom trailing gap. The hidden row stays in the DOM as the structural last child, so the now-visually-last Billing row keeps the bottom margin meant to sit between rows. A gap hangs below the list, spacing against a row no one can see.
<ul className="flex flex-col gap-4"> <li>Profile</li> <li>Billing</li> <li className="hidden">Team settings</li></ul>Correct in every case. gap only spaces the rows that are actually laid out, and the hidden row is pulled out of layout, so no gap is reserved for it. Hide the last one, add a tenth, reorder them: the spacing lives on the parent, not on a per-child selector.
Reading about a phantom gap and seeing one are different things, so trigger it yourself. In the exercise below, the starter is a settings list spaced with space-y-4 whose last item is hidden with the hidden class. That item is still in the DOM, so it stays the structural last child and leaves a stray gap dangling beneath the visible rows. Your job is to convert it to the modern form: swap space-y-4 for flex flex-col gap-4 on the parent, and watch the phantom gap vanish. Match the target on the right.
The starter spaces this list with space-y-4. The last item is hidden with the hidden class, so it stays in the DOM as the structural last child — leaving a stray gap dangling below the visible rows. Convert the parent to flex flex-col gap-4 so the spacing comes from the layout instead of a per-child margin. The phantom gap disappears. Match the target.
Third, margin collapse. space-y-* uses real margins, and real margins collapse. That’s the box-model quirk from the box-model lesson, where two adjacent vertical margins merge into the larger of the two instead of adding. So when a child has its own vertical margin, the space-y margin can collapse against it and quietly under-space the list: you asked for 16px and got 12px, with nothing in the markup to explain the difference. gap is not a margin and never collapses, so the spacing you ask for is the spacing you get, every time. This is a live reason to prefer gap today, not just a historical footnote.
Fourth, right-to-left layouts. A physical sibling margin like margin-right is pinned to the right edge. It doesn’t flip when the document switches to right-to-left, even though the layout around it does, so the spacing ends up on the wrong side. gap is direction-agnostic: it spaces along the layout’s axis whichever way that axis runs, so an RTL flip just works. This is the same logical-versus-physical thread the box-model lesson opened with ps-* and pe-*, and gap gives you that direction-independence in spacing for free.
So here is the rule. space-x-* and space-y-* survive only for the rare parent you genuinely can’t turn into a flex or grid container without side effects, and that parent is much rarer than it sounds, because the clean fix is almost always just adding flex flex-col (or flex) and one gap. The old * + * “lobotomized owl” selector you might spot in hand-written CSS, and space-y itself, are recognition-only: dead for new code, alive in legacy and in AI output trained on it. When you see them, you now know both why they’re there and what to replace them with.
Borders between items with divide
Section titled “Borders between items with divide”gap adds invisible space between items. Sometimes you want a visible line between them instead: the hairline rules between rows in a settings panel, or the thin dividers between clusters in a toolbar. That’s a different question, and gap has no answer for it. divide-* does.
divide-y-* and divide-x-* draw a border between direct children. divide-y puts a border-bottom on each child except the last, so the rules land between the items rather than wrapping the whole container. (That’s the same all-but-the-last mechanism as space-*, which the watch-out below picks up again.) You add divide-color-* to tint the lines, and the whole thing composes with the parent’s own border and rounded-* to produce the classic bordered list card: one rounded, bordered container with hairlines separating the rows inside it.
<ul className="divide-y divide-slate-200 rounded-lg border border-slate-200"> <li className="px-4 py-3">Account</li> <li className="px-4 py-3">Notifications</li> <li className="px-4 py-3">Billing</li></ul>Notice there’s no gap here: the rows sit flush and a hairline separates them. But divide and gap are not rivals; they compose. divide draws a line and gap adds space, and plenty of designs want both: items held a little apart and a rule drawn between them. So divide-* is not a substitute for gap. When you want separation that’s visible, reach for divide; when you want separation that’s just breathing room, reach for gap; when you want both, use both.
When margin still earns its place
Section titled “When margin still earns its place”That leaves margin, which closes the loop on the spatial model. Its surviving role is narrow enough to state precisely, and once you have it, you stop reaching for margin out of habit. Here’s the decision, mechanical enough to apply without thinking:
- Use
gapbetween siblings inside a flex or grid container. This is roughly ninety percent of all spacing you’ll write. - Use
marginto push an element away from something outside its flex or grid container, or in a context that has no flex or grid parent to own agapin the first place. The honest rare cases aremx-autofor centering a lone block (from the box-model lesson) and pushing one element away from a non-sibling neighbor. - Never use
marginbetween siblings inside a flex or grid container. That’sgap’s job, and a sibling margin there only invites the collapse and RTL bugs you just saw.
The senior mental shift is this: padding and gap cover almost everything, and margin is the exception rather than a co-equal third tool. Newcomers reach for margin first, because it’s the spacing property they learned first. Experienced developers reach for gap first and treat a sibling margin as something that needs explaining.
Drill the decision on fresh cases, since applying the rule turns it into a reflex far more than re-reading it does. Sort each spacing situation into the tool you’d reach for.
Sort each spacing situation into the tool you'd reach for first in a 2026 codebase. Drag each item into the bucket it belongs to, then press Check.
flex flex-col listflex rowflex rowmax-w-3xl article in the viewport with mx-autoExternal resources
Section titled “External resources”When you want to go deeper into the Tailwind utility you write, the CSS property behind it, or an interactive feel for where gap lives, these are worth a bookmark.
The utility you actually type: gap, gap-x, and gap-y, with the responsive and arbitrary-value variants.
Hands-on demos that build a real feel for flex layout, including where gap opens its channels between items.
The shorthand for row-gap and column-gap, with how it behaves across flex, grid, and multi-column containers.
The wider alignment-and-spacing model gap belongs to, shared across the flexbox and grid layout systems.