CSS Grid vs Flexbox: When to Use Which
You build a row of product cards with display: flex; flex-wrap: wrap. It looks fine until the last row has two cards instead of four, and those two stretch across the whole width. You fiddle with flex-basis and max-width for twenty minutes, then someone replaces the whole thing with three lines of CSS Grid and it just works. The opposite happens too: a nav bar wrestled into grid columns when Flexbox would've done it in one rule.
This post covers how the two actually differ, which one I reach for in common situations, how they fit together, and a couple of sizing gotchas that trip people up with both.
Flexbox lays things out along one axis (a row OR a column). CSS Grid handles rows AND columns at the same time. Most real pages use both.
Understanding the Core Difference
It comes down to dimensions. Flexbox works along a single axis, horizontal or vertical, and each line of items is sized on its own. Grid defines rows and columns together, so an item in row three lines up with the items above it.
Flexbox arranges items along one axis; Grid controls both rows and columns simultaneously
Side-by-Side Comparison
Flexbox
- One-dimensional layout
- Content-first approach
- Items flow naturally
- Good fit for components
- Item size driven by content
- Easy alignment on one axis
CSS Grid
- Two-dimensional layout
- Layout-first approach
- Explicit placement
- Good fit for page layouts
- Track sizes defined up front
- Items line up across rows
Flexbox in Practice
Flexbox earns its keep when you don't know how big things will be. A button label that might be "OK" or "Save and continue", a username of any length, a variable number of tags: flex distributes whatever space is left and lets items grow or shrink to fit. That's why it shows up mostly at the component level.
Basic Flexbox Setup
.flex-container {
display: flex;
/* Main axis direction */
flex-direction: row; /* row | row-reverse | column | column-reverse */
/* Wrapping behavior */
flex-wrap: wrap; /* nowrap | wrap | wrap-reverse */
/* Main axis alignment */
justify-content: center; /* flex-start | flex-end | center | space-between | space-around | space-evenly */
/* Cross axis alignment */
align-items: center; /* stretch | flex-start | flex-end | center | baseline */
/* Multi-line alignment */
align-content: stretch; /* flex-start | flex-end | center | space-between | space-around | stretch */
/* Gap between items */
gap: 16px;
}
.flex-item {
/* Growth factor */
flex-grow: 1; /* How much item should grow relative to others */
/* Shrink factor */
flex-shrink: 0; /* How much item should shrink relative to others */
/* Base size */
flex-basis: 200px; /* Initial size before growing/shrinking */
/* Shorthand: grow shrink basis */
flex: 1 0 200px;
/* Individual alignment override */
align-self: flex-end;
}
Flexbox Visual Guide
Flexbox alignment properties visualized
Flexbox Use Cases
Navigation Bars
Horizontal menus with flexible spacing between items
Card Components
Vertically stacked content with consistent alignment
Media Objects
Image with text content side-by-side
Equal Height Columns
Columns that stretch to match tallest sibling
Spreading items out along one axis, centering something, or building a component where items should grow or shrink with the space available.
Flex items default to min-width: auto, so they won't shrink below the width of their content. That's why text-overflow: ellipsis "doesn't work" inside a flex row, or a long URL pushes the layout sideways on mobile. Add min-width: 0 (or overflow: hidden) to the item that should shrink. Grid has the same issue: 1fr means minmax(auto, 1fr), so a wide <pre> block or table can blow a column out. Use minmax(0, 1fr) when that happens.
CSS Grid in Practice
With Grid you describe the layout on the container first, then drop items into it. Columns and rows are defined together, so you get precise placement and alignment across both directions. I use it for page shells and for any component where things need to line up in columns.
Basic Grid Setup
.grid-container {
display: grid;
/* Define columns */
grid-template-columns: 200px 1fr 200px; /* Fixed, flexible, fixed */
grid-template-columns: repeat(3, 1fr); /* Three equal columns */
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); /* Responsive! */
/* Define rows */
grid-template-rows: auto 1fr auto; /* Header, content, footer */
/* Gap between cells */
gap: 20px; /* row-gap and column-gap */
/* Named template areas */
grid-template-areas:
"header header header"
"sidebar main aside"
"footer footer footer";
}
.grid-item {
/* Span multiple columns */
grid-column: 1 / 3; /* Start at line 1, end at line 3 */
grid-column: span 2; /* Span 2 columns from current position */
/* Span multiple rows */
grid-row: 1 / 4; /* Start at line 1, end at line 4 */
/* Place in named area */
grid-area: header;
/* Self alignment within cell */
justify-self: center; /* Horizontal alignment */
align-self: center; /* Vertical alignment */
}
Grid Template Areas
Named areas let you draw the layout as ASCII art right in the CSS. Rearranging the page for mobile becomes a matter of rewriting those strings instead of touching every item's placement.
Classic "Holy Grail" layout using grid-template-areas
.holy-grail {
display: grid;
min-height: 100vh;
gap: 16px;
grid-template-columns: 200px 1fr 200px;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header header"
"sidebar main aside"
"footer footer footer";
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.aside { grid-area: aside; }
.footer { grid-area: footer; }
/* Responsive: Stack on mobile */
@media (max-width: 768px) {
.holy-grail {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"sidebar"
"aside"
"footer";
}
}
Auto-Fit and Minmax: Responsive Without Media Queries
This is the trick that fixes the card problem from the intro. auto-fit plus minmax() gives you a grid that picks its own column count, no media queries involved. Keep in mind the column counts in the comment below are rough: padding and gap eat into the width, so the real breakpoints land a bit differently. If you'd rather keep empty column slots instead of stretching the cards when there are only a few, use auto-fill.
.card-grid {
display: grid;
/* Magic responsive formula */
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
padding: 24px;
}
/*
This creates a grid where:
- Each card is minimum 280px wide
- Cards grow equally to fill available space (1fr)
- Grid automatically adjusts number of columns
- No media queries needed!
On a 1200px screen: 4 columns
On a 900px screen: 3 columns
On a 600px screen: 2 columns
On a 400px screen: 1 column
*/
Grid Use Cases
Page Layouts
Complex layouts with headers, sidebars, and footers
Image Galleries
Pinterest-style layouts with varied sizes
Data Tables
Aligned columns with consistent widths
Dashboard Layouts
Widgets of different sizes in a grid
Decision Guide: Grid or Flexbox?
When I'm unsure, I run through roughly these questions:
Using Grid and Flexbox Together
You don't have to pick one per project. A common split is Grid for the page structure and Flexbox for the components sitting in the grid cells.
Page layout with Grid, components inside with Flexbox
/* Page layout with Grid */
.page {
display: grid;
min-height: 100vh;
grid-template-columns: 250px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
}
/* Navigation with Flexbox */
.header {
grid-area: header;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
}
.nav-links {
display: flex;
gap: 24px;
}
/* Card grid inside main area */
.main {
grid-area: main;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
padding: 24px;
}
/* Card content with Flexbox */
.card {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
background: var(--bg-secondary);
border-radius: 12px;
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
}
.card-footer {
display: flex;
justify-content: space-between;
margin-top: auto; /* Push to bottom */
}
Common Patterns
Centering Content (The Classic Problem)
.center-flexbox {
display: flex;
justify-content: center; /* Horizontal */
align-items: center; /* Vertical */
min-height: 100vh;
}
.center-grid {
display: grid;
place-items: center; /* Both axes at once! */
min-height: 100vh;
}
/* Or */
.center-grid-alt {
display: grid;
place-content: center;
}
Sticky Footer
.page-wrapper {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
.header { /* auto height */ }
.main { /* takes remaining space (1fr) */ }
.footer { /* auto height, always at bottom */ }
Equal-Height Cards in a Row
.card-row {
display: flex;
gap: 24px;
}
.card {
flex: 1; /* Equal width */
display: flex;
flex-direction: column;
}
.card-body {
flex: 1; /* Fills remaining space */
}
.card-footer {
margin-top: auto; /* Always at bottom */
}
Quick Reference Cheat Sheet
| Use Case | Recommended | Why |
|---|---|---|
| Page layout (header, sidebar, main, footer) | Grid | Two-dimensional control needed |
| Navigation bar | Flexbox | Single row, flexible spacing |
| Card grid that wraps | Grid | auto-fit keeps alignment |
| Centering a single element | Grid | place-items: center is simplest |
| Items with varying sizes in a row | Flexbox | Content-based sizing |
| Form layout with labels/inputs | Grid | Aligned columns |
| Vertical stack with spacing | Flexbox | Simple, single axis |
| Pinterest-style masonry | Grid | grid-auto-rows with varying heights |
- Using Grid for everything: Not every layout needs two dimensions. A simple row is usually less code with Flexbox.
- Margins instead of gap: Both Grid and Flexbox support
gap, and it doesn't leave you with a stray margin on the last item. - Skipping minmax(): For responsive grids,
minmax(250px, 1fr)saves you a pile of media queries. - Over-nesting: A flex container inside a flex container inside a grid cell is usually a sign one level could go.
When I'd Pick Each (and When I Wouldn't)
I default to Flexbox for anything that's a single row or column of stuff whose size depends on its content: nav bars, button groups, toolbars, a card's internal stack. I wouldn't use it for a wrapping card list where the last row needs to line up with the rows above. That's exactly the case where flex fights you and Grid doesn't.
I reach for Grid when items have to align in both directions: page shells, dashboards, forms with a label column and an input column, card grids. I'd skip it for a row of tags or chips whose count and widths vary a lot. Grid wants you to think in tracks, and a wrapping flex row handles that kind of content with less ceremony.
One thing to keep an eye on with Grid: if you rearrange items visually with grid-template-areas or order, the tab order and screen reader order still follow the HTML source. Moving the sidebar above the main content on mobile is fine visually, but check it with the keyboard before you ship. And if a layout looks broken and you can't tell why, turn on the Grid and Flex overlays in your browser's devtools. They show the actual tracks and free space, which usually explains the problem faster than reading the CSS.