CSS Grid vs Flexbox: How to Choose for Each Layout
Grid and Flexbox overlap enough that either can produce most simple layouts, which makes the choice feel arbitrary. It is not: they size items in opposite directions, and that difference decides how a layout behaves when the content, the viewport, or the number of items changes. This guide gives you a default, the criteria behind it, and live examples you can edit.
Quick answer: Use Grid when the container defines the layout: you know the columns or rows, and items must line up across them (page shells, card galleries, forms, dashboards). Use Flexbox when the content defines the layout: items of varying size flow along one line and share the leftover space (toolbars, navigation, tag lists, button groups). Most real interfaces use Grid for the structure and Flexbox inside the pieces.
Difficulty: Intermediate
You'll understand this better if you know: Flexbox basics, Grid basics (tracks, fr, gap), and the box model.
1. The Real Difference: Who Sizes the Items?
"Flexbox is one-dimensional, Grid is two-dimensional" is true, but it is a consequence. The underlying difference is where sizes come from.
- Flexbox works from the content out. Each item starts from a flex base size, taken from flex-basis (with auto, its width, or its content size if the width is also auto). The container then hands out the remaining space, or takes back the overflow, using flex-grow and flex-shrink. When items wrap, each line repeats this on its own, so items on the second line know nothing about the first.
- Grid works from the container in. The container defines tracks, and items are placed into the areas between grid lines. Tracks can be fixed, flexible (fr), or content-based (auto, min-content, max-content, fit-content()). A content-based track is sized from every item in it, so items in the same column share a width and items in the same row share a height.
That is why Grid aligns things in two directions: tracks are shared. Flex lines are independent, so nothing lines up across them unless you force it with fixed sizes. MDN's Relationship of grid layout to other layout methods describes the same split as "layout in" versus "content out".
2. Decision Criteria at a Glance
| Criterion | Flexbox | Grid |
|---|---|---|
| Sizing source | Content first; free space shared by flex-grow/flex-shrink | Track definitions first; content-based tracks consider every item in the track |
| Dimensions | One axis at a time | Rows and columns together |
| Alignment across wrapped lines | None: each line sizes and aligns itself | Built in: items in a column share its width |
| Wrapping | flex-wrap; a short last line can stretch | repeat(auto-fill, minmax()); wrapped items keep the column width |
| Items with different natural widths in one row | Natural fit | Forced into tracks |
| Explicit placement, spanning, overlap | No (only order) | grid-column, grid-row, named areas, overlapping items |
| Aligning nested content across siblings | No | Yes, with subgrid |
| Pushing an item to the end | margin-inline-start: auto | An auto margin, or a 1fr track before it |
| Responsive without media queries | Breakpoints emerge from flex-basis and content | Column count emerges from minmax() track sizes |
| Default minimum size of items | min-width: auto (content-based) | min-width: auto, and 1fr means minmax(auto, 1fr) |
| Visual reordering | order, *-reverse; does not change reading or focus order | Placement, order, dense packing; same caveat |
3. The Trade-offs in Detail
Content-driven vs layout-driven sizing
The flex shorthand decides how much content influences the result, and the common values behave very differently (see the Flexbox specification):
- flex: 1 means 1 1 0: every item starts from zero and receives an equal share, so widths ignore content (until an item's minimum content width stops it from shrinking).
- flex: auto means 1 1 auto: items start from their content size and share the leftover, so wider content stays wider.
- flex: none means 0 0 auto: the item keeps its width (or its content size if the width is auto) and neither grows nor shrinks.
Grid is not limited to fixed sizes. auto, max-content, and fit-content() tracks size to content too; the difference is that the decision is made once for the whole track rather than per item. The form example in section 4 relies on exactly that.
Wrapping: independent lines vs shared columns
This is the difference people hit first. Both containers below hold seven cards with the same 140px minimum. Open the preview, then edit the widths or the number of cards:
<style>
.label { margin: 0 0 6px; font: 600 13px/1.4 system-ui, sans-serif; }
.demo { max-width: 520px; margin-bottom: 16px; gap: 8px; }
.card { box-sizing: border-box; padding: 8px 12px; border: 1px solid #93c5fd; border-radius: 6px; background: #dbeafe; font: 14px system-ui, sans-serif; }
.flex-cards { display: flex; flex-wrap: wrap; }
.flex-cards .card { flex: 1 1 140px; }
.grid-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.grid-cards .card { border-color: #86efac; background: #dcfce7; }
</style>
<p class="label">Flexbox: flex: 1 1 140px with flex-wrap</p>
<div class="demo flex-cards">
<div class="card">1</div><div class="card">2</div><div class="card">3</div>
<div class="card">4</div><div class="card">5</div><div class="card">6</div>
<div class="card">7</div>
</div>
<p class="label">Grid: repeat(auto-fill, minmax(140px, 1fr))</p>
<div class="demo grid-cards">
<div class="card">1</div><div class="card">2</div><div class="card">3</div>
<div class="card">4</div><div class="card">5</div><div class="card">6</div>
<div class="card">7</div>
</div>With Flexbox, the seventh card sits alone on the last line, and because each line distributes its own free space, it grows to the full width. With Grid, the column widths come from the container, so the seventh card stays in the first column at the same width as the others. If the stretched last card is what you want (for example, buttons that should fill a row), Flexbox is right. If wrapped items should form columns, use Grid.
Note: With fewer items than fit in one row, auto-fill keeps the empty tracks, while auto-fit collapses them so the existing items stretch across the row.
Minimum sizes and overflow
Both models share a default that causes many "my layout overflows" bugs: flex items and grid items have min-width: auto, which resolves to their content-based minimum. A long URL or a wide code block then forces the item wider than its share. In Grid, a bare 1fr track has an auto minimum for the same reason.
/* Flex and grid items default to min-width: auto, so a long URL or a wide
<pre> can force an item wider than its share and overflow the page. */
.layout-flex > main {
flex: 1;
min-width: 0; /* allow the item to shrink below its content */
}
.layout-grid {
display: grid;
grid-template-columns: 16rem minmax(0, 1fr); /* a bare 1fr means minmax(auto, 1fr) */
}
main pre {
overflow-x: auto; /* the long line now scrolls inside its box */
}Setting overflow to a scrolling value on the item itself also removes the automatic minimum, because both specifications exempt scroll containers.
Source order and accessibility
Both models can show items in a different order from the HTML: order and the *-reverse directions in Flexbox, and line placement, named areas, order, and grid-auto-flow: dense in Grid. Screen readers and keyboard focus still follow the DOM. The Grid and Flexbox specifications both say to use these features "only for visual, not logical, reordering of content" (CSS Grid Level 2). Grid's placement power makes this easier to get wrong, so if the visual order differs from the reading order, change the HTML.
Browser behavior and support
Flexbox and Grid are supported in every current browser. Two newer pieces are worth checking against your support matrix: gap in flex containers (Chrome 84, Firefox 63, Safari 14.1) and subgrid (Chrome and Edge 117, Firefox 71, Safari 16). Performance is not a reason to pick one: both are native layout algorithms, and for normal interfaces you should choose on behavior and only investigate if the Performance panel shows long layout work.
4. Realistic Scenarios
Scenario 1: A site header or toolbar → Flexbox
A logo, a search field, a few links, and an account menu. The number of items changes between pages and their widths come from their text.
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 1rem;
}
.toolbar .search {
flex: 1 1 12rem; /* takes the leftover space; wraps below 12rem */
}
.toolbar .account {
margin-inline-start: auto; /* pushed to the far end of its line */
}Why Flexbox: every item keeps its natural width, the search field absorbs the leftover space, the account menu is pushed to the end with an auto margin, and the whole bar wraps gracefully. Grid would require you to decide the columns up front and update them whenever an item is added.
Scenario 2: A product or card gallery → Grid
An unknown number of cards that should form tidy columns at any viewport width.
Why Grid: repeat(auto-fill, minmax(14rem, 1fr)) picks the column count from the available width without media queries, every card in a column has the same width, and the last row does not stretch, as the wrapping example above showed. Add subgrid when titles, prices, and buttons inside the cards must line up too.
Scenario 3: A form with aligned labels → Grid
Labels of different lengths next to inputs. The label column should be as wide as the longest label, and every input should start at the same position.
<style>
.label { margin: 0 0 6px; font: 600 13px/1.4 system-ui, sans-serif; }
.form { max-width: 520px; margin: 0 0 16px; font: 14px system-ui, sans-serif; }
.form input { padding: 6px 8px; border: 1px solid #9ca3af; border-radius: 4px; font: inherit; }
/* Flexbox: every row is its own flex container */
.flex-row { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.flex-row input { flex: 1; min-width: 0; }
/* Grid: one container, one shared label column */
.form-grid { display: grid; grid-template-columns: max-content minmax(0, 1fr); align-items: center; gap: 8px 12px; }
</style>
<p class="label">Flexbox: each row sizes its own label</p>
<form class="form">
<div class="flex-row"><label for="f-name">Name</label><input id="f-name"></div>
<div class="flex-row"><label for="f-email">Email address</label><input id="f-email"></div>
<div class="flex-row"><label for="f-phone">Phone</label><input id="f-phone"></div>
</form>
<p class="label">Grid: the label column fits the longest label</p>
<form class="form form-grid">
<label for="g-name">Name</label><input id="g-name">
<label for="g-email">Email address</label><input id="g-email">
<label for="g-phone">Phone</label><input id="g-phone">
</form>Why Grid: this is a content-driven layout that still needs alignment across rows. Each flex row sizes its own label, so the inputs start at different positions. One grid with a max-content label column sizes that column from all labels at once. The usual Flexbox fix, a fixed label width, breaks as soon as a translation or a longer label appears.
Scenario 4: Tags, chips, and button groups → Flexbox
A list of tags with different text lengths that wraps onto as many lines as needed.
/* Flexbox: every tag is as wide as its text */
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
/* Grid would force every tag into an equal-width column:
.tags { display: grid; grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr)); } */Why Flexbox: the tags should be exactly as wide as their text, and nothing needs to align across lines. A grid would put each tag into an equal-width column, wasting space around short tags and squeezing long ones.
5. Decision Checklist
Go through the questions in order and stop at the first "yes":
- Must items line up with each other in both directions, such as columns that stay aligned across rows? Use Grid.
- Do you need to place items in specific cells, span several tracks, or overlap items? Use Grid.
- Must content inside sibling components line up, such as card titles and footers? Use Grid with subgrid.
- Is it a single row or column whose items should keep their natural size and share the leftover space? Use Flexbox.
- Do items wrap? If wrapped items should form consistent columns, use Grid with auto-fill or auto-fit and minmax(). If each line may size itself, use Flexbox with flex-wrap.
- Is the number of items unknown and their size driven by their text, such as navigation links or actions? Use Flexbox.
- Still unsure? Use Grid for the outer structure of the page or component and Flexbox for the items inside each region.
6. Wrong Reasons to Pick One
- "Grid is for pages, Flexbox is for components." Size is not the criterion. A three-field form is a small component and a Grid problem; a site header spans the page and is a Flexbox problem.
- "Grid is newer, so it replaces Flexbox." They are complementary specifications that solve different problems, and both are current.
- "I need wrapping, so I need Flexbox." Grid wraps too, with auto-fill and auto-fit. The question is whether wrapped items should align.
- "Grid needs fixed sizes." auto, max-content, and fit-content() tracks are content-based.
- "Only Flexbox can center things." display: grid; place-items: center; centers a child in both directions as well.
- "I'll use order to rearrange things for mobile." That changes the visual order only; keyboard and screen reader users get the DOM order. Reorder the HTML instead.
- "One of them is faster." Not in a way that should drive the decision. Choose on behavior.
7. When to Mix Both
Mixing is the normal case, not a compromise. Any grid item can be a flex container and any flex item can be a grid container. The pattern that holds up best is Grid for regions and repeated structures, and Flexbox for the row or column of items inside each region:
<style>
.app {
display: grid;
grid-template-columns: 120px minmax(0, 1fr);
grid-template-areas:
"header header"
"nav main";
gap: 8px;
font: 14px/1.4 system-ui, sans-serif;
}
/* Flexbox inside the header: content-sized items, actions pushed to the end */
.app-header {
grid-area: header;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 6px;
background: #1e3a8a;
color: #fff;
}
.app-header .actions { display: flex; gap: 6px; margin-inline-start: auto; }
.app-header button { padding: 4px 10px; border: 0; border-radius: 4px; font: inherit; }
.app-nav { grid-area: nav; display: flex; flex-direction: column; gap: 4px; }
.app-nav a { padding: 4px 8px; border-radius: 4px; background: #e0e7ff; color: #1e3a8a; text-decoration: none; }
/* Grid for the tiles; subgrid lines up titles, text, and footers across tiles */
.app-main { grid-area: main; display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.tile {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
gap: 4px;
padding: 10px;
border: 1px solid #cbd5e1;
border-radius: 6px;
}
.tile h3 { margin: 0; font-size: 15px; }
.tile p { margin: 0; color: #374151; }
.tile footer { display: flex; gap: 6px; }
.tile footer span { padding: 2px 6px; border-radius: 4px; background: #f1f5f9; font-size: 12px; }
@media (max-width: 480px) {
.app { grid-template-columns: minmax(0, 1fr); grid-template-areas: "header" "nav" "main"; }
.app-nav { flex-direction: row; flex-wrap: wrap; }
}
</style>
<div class="app">
<header class="app-header">
<strong>Acme</strong>
<span>Projects</span>
<div class="actions"><button type="button">Share</button><button type="button">New</button></div>
</header>
<nav class="app-nav">
<a href="#">Overview</a><a href="#">Tasks</a><a href="#">Settings</a>
</nav>
<main class="app-main">
<article class="tile">
<h3>Launch plan</h3>
<p>Short summary.</p>
<footer><span>Due Fri</span></footer>
</article>
<article class="tile">
<h3>Customer research interviews</h3>
<p>A longer description that wraps onto several lines in a narrow tile.</p>
<footer><span>12 notes</span><span>3 owners</span></footer>
</article>
<article class="tile">
<h3>Docs</h3>
<p>Medium length text here.</p>
<footer><span>Draft</span></footer>
</article>
</main>
</div>In this example:
- Grid defines the shell with named areas, and a media query switches it to a single column on narrow screens without touching the HTML.
- Flexbox lays out the header, where items are sized by their text and the actions are pushed to the end with margin-inline-start: auto, and the navigation, which changes from a column to a wrapping row.
- Grid arranges the tiles, and grid-template-rows: subgrid lets each tile reuse three rows of the parent grid, so titles, descriptions, and footers line up across tiles even though the text lengths differ.
- Flexbox returns inside each tile footer for the small badges.
8. Key Points
- Flexbox sizes from the content out; Grid sizes from the container in. "One- vs two-dimensional" follows from that.
- Use Grid when items must align across rows and columns, when you need placement or spanning, and when nested content must line up (subgrid).
- Use Flexbox for a single line of items with natural sizes that share leftover space: toolbars, navigation, tags, button groups.
- Wrapped flex lines are independent, so a short last line stretches; Grid keeps wrapped items in columns.
- Grid can be content-driven too, with auto, max-content, and fit-content() tracks.
- Both default to min-width: auto for items; use min-width: 0 or minmax(0, 1fr) when long content overflows.
- Visual reordering in either model does not change reading or focus order.
- Grid for structure and Flexbox inside each region is the default combination.
9. Final Summary
Choosing between Grid and Flexbox is choosing who decides the sizes. When the container should decide, because items must form columns, align across rows, span areas, or share internal structure through subgrid, use Grid. When the items should decide, because each one is as wide as its content and the line simply distributes what is left, use Flexbox.
The checklist above resolves most cases in a few questions, and the cases it does not resolve are usually layouts that need both: Grid for the regions and repeated structures, Flexbox for the items inside them. Keep the HTML in reading order, watch for the min-width: auto overflow trap, and let the content-versus-layout question make the call rather than habit.