Using the gap Property for Spacing in CSS Flexbox and Grid Layouts

The CSS gap property provides spacing between flexbox and grid items without margin hacks or negative container margins. Use it with a fallback for older browsers.

You are still writing margin-right: 16px on every card and then fighting the trailing gutter with a negative margin on the wrapper, or you are using the lobotomised owl selector and hoping the last item behaves. The CSS gap property replaces that 2014 hack with one declaration on the parent. It has been widely available across all major engines since 2024. The gap property works identically in flexbox and grid: it adds space between items only, never at the container edges, and it respects the content box so the first and last items sit flush against the padding.

The CSS Gap Property Flexbox Grid: What It Replaces and Why It Matters

How the Margin Hack Fails on Wrapped Layouts

Before gap, the standard pattern for spacing a row of flex items was margin on the children. You set margin-right: 16px on every item except the last one, or you used the lobotomised owl selector (.item + .item { margin-left: 16px; }) to avoid the trailing edge problem. Both work. Both break when the layout wraps.

With flex-wrap: wrap, the margin-right on every item creates an unwanted gutter at the end of each line, and the negative margin on the wrapper only cancels the outer edge, not the internal line breaks. The gap property solves this. It applies only between items on the same flex line, and it does not create space at the container edges. In grid, gap replaces the older grid-gap property, and it works the same way: space between tracks, never outside the grid container.

Row-Gap Column-Gap Flexbox: The Shorthand and the Longhand

The gap shorthand takes one or two values. A single value sets both row-gap and column-gap. Two values set row-gap first, then column-gap, matching the margin and padding ordering for block and inline directions.

In a flex container with flex-direction: row, row-gap controls the space between flex lines when wrapping occurs, and column-gap controls the space between items on the same line. In grid, row-gap sets the space between row tracks, and column-gap sets the space between column tracks.

Percentage Values and the Normal Keyword

The percentage value for gap refers to the content area size of the container: for row-gap it is a percentage of the container’s height, for column-gap it is a percentage of the width. This matters when the container has no explicit height, because a percentage row-gap then computes to zero, and the spacing silently disappears. The normal keyword computes to 0 in flexbox and grid, but in multi-column layout it computes to 1em for column-gap, which is the default gap between columns.

Gap Property Browser Support: The Numbers and the Baseline Status

Shipping Dates and the Baseline Window

Gap in flexbox shipped in Chromium 84, Safari 14.1, and Firefox 63. Grid gap shipped much earlier: Chrome 66, Firefox 61, and Safari 12 shipped the prefixed grid-gap property, and the unprefixed gap became available in those engines shortly after.

The Baseline status for flexbox gap is widely available as of 2024, which means it works in the current and previous major versions of all core browsers. MDN publishes the compatibility data that feeds Baseline, and the status is rolled on a 30-month window: a feature is widely available once it has shipped in all engines and 30 months have passed without a major interop break.

The Grid-Gap Aliases

The grid-gap prefixed properties were renamed to gap, row-gap, and column-gap, and the old names are now aliases, not separate features. If you see grid-gap in a stylesheet, treat it as the same property, but prefer the unprefixed name because the aliases may be dropped in future engine releases.

Gap vs Margin Spacing: The Layout Cost and the Wrap Behaviour

The performance difference between gap and margin is not in the initial layout, which is comparable for both, but in what happens when the container resizes. With margin on flex items, a resize of the container re-flows every item and re-computes every margin. With gap on the container, the engine knows the track spacing as a property of the flex container or grid container itself, and it can distribute the free space without visiting each child’s margin box.

In grid, gap affects the track sizing algorithm. The gap participates in the fr unit distribution. With margins, the tracks are sized first and the margins are applied after, which can cause overflow when the total exceeds the container width. With gap, the tracks shrink to accommodate the spacing, and the flex lines or grid tracks never overflow because of the gutter. This is the real divide between the two approaches: margin is an afterthought on the children, gap is a first-class constraint on the container.

Flexbox Gap Fallback: The @supports Pattern and the Margin Hack

For browsers that shipped flexbox before gap, the fallback is the margin hack, and you gate it with @supports. Write the margin-based spacing first, then use @supports (gap: 1rem) to override with the gap property and remove the margins. In code, that looks like this:

.flex-list {
  display: flex;
  flex-wrap: wrap;
  margin: -8px; /* cancels the outer margins */
}
.flex-list > * {
  margin: 8px; /* the hack */
}
@supports (gap: 1rem) {
  .flex-list {
    gap: 1rem;
    margin: 0; /* remove the negative margin */
  }
  .flex-list > * {
    margin: 0; /* remove the child margins */
  }
}

The @supports test for gap: 1rem fails in browsers that only support the prefixed grid-gap, because the gap property is not parsed there. For those older engines, you need a second @supports block with the prefixed property, or you rely on the margin fallback as the baseline. In practice, the prefixed grid-gap only ever shipped for grid, never for flexbox, so if you need flexbox gap in an engine that lacks it, the margin hack is your only option.

The @supports selector() function can also test for the property, but it does not help here because gap is a property, not a selector, and the selector() test is for features like :has(). The fallback pattern above is the correct one: it degrades to the margin approach in any browser that does not parse gap, and it upgrades to the native spacing where available. Do not use a JavaScript polyfill for gap; the margin approach is straightforward, and the polyfill would need to observe the DOM and re-apply margins on every resize, which is worse for performance than the native property.

A Sample Demonstrating Gap in Flexbox with Wrapping

Here is a flex container with wrapping, using gap for the gutters. The container has a fixed width, and the items have a basis that forces them onto multiple flex lines. With the margin hack, the last item on each line would have an unwanted right margin, and the negative margin on the container would create a visual misalignment. With gap, the space between items is uniform, and the container edges stay clean.

<div class="flex-cards">
  <div class="card">Card 1</div>
  <div class="card">Card 2</div>
  <div class="card">Card 3</div>
  <div class="card">Card 4</div>
</div>
.flex-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem; /* row-gap and column-gap both 1rem */
}
.card {
  flex: 1 1 200px;
  /* no margin needed */
}

The flex line spacing is controlled by row-gap, and the in-line spacing by column-gap. If the container is narrower than the total of the cards, the cards wrap, and the row-gap creates a vertical gutter between the rows. The column-gap creates the horizontal gutter between cards on the same row. The container has no negative margin, and the cards have no margin, so the only spacing is the gap, which the engine applies between the items. This is the core difference from the margin hack: the flex container owns the spacing, and the items do not participate in the gutter calculation.

A Sample Showing Gap in Grid with Track Spacing

In grid, gap is part of the track sizing. The grid container has explicit columns, and the gap creates space between the tracks without affecting the container edges. The following sample shows a three-column grid with a 1rem gap.

<div class="grid-cards">
  <div>Item A</div>
  <div>Item B</div>
  <div>Item C</div>
  <div>Item D</div>
  <div>Item E</div>
  <div>Item F</div>
</div>
.grid-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem; /* row-gap and column-gap */
}

The 1fr tracks share the available space after the gap is subtracted. With margins on the grid items, the tracks would each be sized at a full fraction, and the margins would push the total beyond the container width, causing overflow. The gap property prevents this because the track sizing algorithm includes the gap as a fixed consumption. The same applies to row tracks: the row-gap is subtracted from the container height before the tracks are sized.

Common Mistake 1: Percentage Gaps on a Container Without Explicit Height

A percentage row-gap resolves against the container’s height. If the container has no explicit height and its height is determined by its content, the percentage computes to zero, and the row-gap disappears. This is a silent failure: the layout looks correct in a context where the container gets a height from an ancestor, but breaks in a context where the height is auto.

Fix it by using a length value for row-gap, or by giving the container a definite height. For column-gap, the percentage resolves against the width, which is usually definite in a block layout, so it is less of a problem. If you need a percentage for the column gap, test it with a container that has a known width, and never use a percentage for row-gap in a flex container with auto height. The spec says percentages refer to the content area size of the container, and the content area of a flex container with auto height is its used height, which is often indefinite.

Common Mistake 2: Applying Gap to a Non-Container

The gap property applies to multi-column containers, flex containers, and grid containers. It does nothing on a regular block container or an inline element. A common mistake is setting gap on a div that is display: block, expecting it to space the child elements. It will not. The children will sit at their default margins, and the gap declaration is ignored.

Fix it by changing the display to flex or grid, or by using margin on the children. The multi-column container (column-count or column-width) responds to column-gap but not row-gap, because multi-column layout has no rows. The normal value in multi-column is 1em, while in flex and grid it is 0. If you intend to space items in a normal block flow, gap is not the tool; the lobotomised owl selector or margin on the children is.

Gap Property Browser Support: The Real Interop Gaps Beyond the Shipped Dates

The shipping dates tell you when the feature landed, not when it became reliable. Flexbox gap shipped in Chromium 84, Safari 14.1, and Firefox 63, but the early implementations had bugs. Firefox 63 supported gap in flexbox, but the row-gap property was not applied to wrapped flex lines until Firefox 69. Safari 14.1 had a bug where gap combined with align-content: space-between double-counted the space, which was fixed in a later point release.

The Baseline widely available status as of 2024 tells you that the feature works in the current and previous major versions, but it does not tell you about the minor version bugs that were fixed after shipping. If you are targeting Safari 14.1 specifically, test the combination of gap with justify-content and align-content, because the early version had known issues with space distribution. The MDN compatibility data lists these sub-feature bugs, and the Baseline status is a rolling window, not a static guarantee. A feature can be widely available and still have an edge case that fails in a specific minor version of an engine that is still in use on an older device.

Flexbox Gap Fallback: The @supports Pattern and the Margin Hack

The fallback pattern exists because the margin hack is the only way to get spacing in browsers that shipped flexbox without gap. The hack works, but it has a cost: the negative margin on the container must match the sum of the child margins divided by the number of lines, which is not possible when the number of lines is unknown.

For a single-line flex container, the negative margin is -8px on the container and 8px on each child, which cancels the outer edges. For a multi-line container, the negative margin cancels the outer edges only if every line has the same number of items, which is rare. The correct fallback for multi-line is to accept the trailing gutter on the last item of each line, or to use a CSS grid with explicit rows. The @supports block with gap: 1rem removes the hack in modern engines, and the margin approach remains for the legacy ones. This is the pattern that the Baseline status makes redundant for most users, but if you support a view that includes an iOS Safari locked to a device, the margin fallback is what keeps the layout intact.

Gap vs Margin Spacing: The Real Cost in Layout and Paint

The performance cost of gap compared to margin is measurable in the layout phase, not the paint phase. Margin on flex items requires the engine to compute each item’s margin box, which is a per-child operation. Gap is a container-level property, and the engine can distribute the space between items in a single pass.

In grid, the difference is larger because gap affects the track sizing algorithm, while margins are applied after the tracks are sized. The margin approach can cause the grid to overflow, which forces a second calculation of the grid tracks, a layout thrash. Gap prevents this because the track sizing knows the gap from the start. The paint cost is the same for both: the engine paints the same pixels. The composite cost is also the same. The real divide is in the layout cost, which is why the spec defines gap as a property of the container rather than a margin on the children. If you have a large grid with hundreds of tracks, the margin approach can double the layout time because of the overflow re-calculation. With gap, the layout is single-pass.

FAQ: Seven Questions About the Gap Property in Flexbox and Grid

Here are the answers to the questions that come up most often when switching from margin hacks to gap.

When did gap become available in flexbox across all major engines?

Gap in flexbox shipped in Chromium 84 (July 2020), Safari 14.1 (April 2021), and Firefox 63 (October 2018). The Baseline status is widely available as of 2024, meaning it is safe to use without a fallback for current browser versions.

Is gap the same property in flexbox and grid?

Yes. The gap shorthand, and its longhands row-gap and column-gap, apply to both flex containers and grid containers. The behaviour is identical: space between items, never at the container edges. The only difference is that grid has track spacing in two axes, while flexbox has it along the main axis and between flex lines.

Does gap work with flex-wrap?

Yes. With flex-wrap: wrap, the row-gap creates a vertical gutter between flex lines, and the column-gap creates a horizontal gutter between items on the same line. This is the primary advantage over the margin hack, which cannot create a clean gutter between wrapped lines.

What is the fallback for gap in older browsers?

The fallback is the margin hack: set margin on the child items and a negative margin on the container to cancel the outer edges. Use @supports (gap: 1rem) to override the margins with the gap property in browsers that support it. This pattern is the correct one for pre-2020 browsers.

Is grid-gap the same as gap?

Grid-gap, grid-row-gap, and grid-column-gap are the legacy prefixed names for gap, row-gap, and column-gap. They are aliases, not separate properties. The unprefixed names are the standard, and the aliases may be removed in future engine releases, so use the unprefixed versions.

Why does my percentage gap not work?

A percentage row-gap resolves against the container’s height. If the container has no explicit height, the percentage computes to zero. column-gap resolves against the width, which is usually definite. Use a length value for row-gap, or give the container a definite height.

Does gap affect the alignment of items with justify-content or align-content?

Yes. Gap adds to the free space that justify-content and align-content distribute. For example, with justify-content: space-between and a gap, the engine first subtracts the total gap from the container width, then distributes the remaining free space between the items. This can change the spacing you expect, so test the combination together.

The Practical Takeaway and the Honest Caveat

Use gap for all new flex and grid layouts. It is widely available, it is the standard, and it removes the margin hack that has caused wrapping bugs for a decade.

The one caveat: gap does not replace margin for alignment purposes. If you need to push an item to the edge of the container, margin-left: auto or justify-content: space-between is still the tool. Gap only creates space between items, never at the edges, and it cannot be used to align a single item.

The failure case is applying gap to a non-container, or using a percentage row-gap without a container height, both of which silently do nothing. The honest caveat is that the margin hack is not wrong, it is obsolete. If you are supporting a browser that predates 2020, the margin approach works, and the @supports fallback is the professional way to migrate. The gap property is the fastest alignment, and the fallback is the cost you pay for supporting a device-locked browser that has not updated in four years.