Grid vs Flexbox: The Decision, Not the Feature List
The rule that actually decides it: Flexbox distributes space along one axis; Grid places items on two. Content-first versus container-first is the common summary and it is a consequence of that, not the rule itself.
Decide in one question
| Question | Use |
|---|---|
| Are items laid out in a single row or column that wraps? | flex |
| Do items need to line up with items in another row? | grid |
| Does the content get to decide how much space each item takes? | flex |
| Does you decide, and content must fit the tracks? | grid |
| One-dimensional layout inside a component that lives in a grid cell? | flex, inside the cell |
| Two-dimensional page skeleton? | grid |
If you are writing flex-wrap: wrap and giving children width: calc(33.3% - 1rem), you have rebuilt Grid with arithmetic and lost gap, alignment, and the ability to change columns in one declaration.
Properties that do not exist in the other model
| Grid only | Flex only |
|---|---|
grid-template-areas, named lines | flex-grow, flex-shrink, flex-basis |
grid-auto-flow: dense | justify-content: space-between on the main axis with implicit sizing |
subgrid | order — exists in grid too, but only behaves sanely in flex |
align-content on both axes | flex-flow shorthand |
place-items / place-content | baseline alignment along the main axis |
flex-basis: 0 + flex-grow: 1 is how you get equal-width flex items — without it, items size to content and the row looks like it "refuses to be equal". That one misunderstanding is most of the grid-vs-flexbox argument people have online.
The four patterns worth memorising
/* 1. Responsive column count, zero media queries */
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; }
/* auto-fit collapses empty tracks so a single item can stretch; auto-fill keeps the grid */
/* 2. The sidebar that never wins an argument */
.app { display: grid; grid-template-columns: minmax(0, 1fr) 18rem; gap: 1.5rem; }
/* minmax(0,1fr) matters: bare 1fr has min-width:auto and cannot shrink below content */
/* 3. Sticky footer without a wrapper */
body { display: grid; grid-template-rows: auto 1fr auto; min-height: 100dvh; }
/* 4. Toolbar: items left, one pushed right, no float, no spacer div */
.bar { display: flex; align-items: center; gap: .75rem; }
.bar > .push { margin-inline-start: auto; }
Pattern 4 deserves the note: margin-inline-start: auto on the item beats justify-content: space-between, because space-between distributes leftover space across all gaps and stops working the moment a second group appears. auto margin is per item and survives adding controls.
Where each model bites
Flexbox:
min-width: autoon flex items means a long unbreakable string (a URL, a code line) cannot shrink; the row overflows and no scroll container exists to save you. Fix on the item:min-width: 0plusoverflow: auto.flex-direction: column+align-items: stretchdoes not stretch a child that has an intrinsic size — that is why a nested<svg>in a column flex container ignoreswidth: 100%until you give italign-self: stretch.- Wrapping with
gapandalign-contentbehaves differently fromalign-items:align-itemspositions items within a line,align-contentpositions the lines. A wrapped flex row that "ignores"align-items: centeris almost always fixed by also settingalign-content: start.
Grid:
- Implicit tracks (items placed beyond your template) get
grid-auto-rows: auto, so a 3-column grid with 7 items has a mystery fourth row. Either declare what you use or setgrid-auto-flow: row densedeliberately —densereorders visually and breaks reading order for screen-reader and keyboard users who follow DOM order. 1frisminmax(auto, 1fr), which is the source of nearly every "my grid item will not shrink" report.minmax(0, 1fr)on the track, ormin-width: 0on the item.- Percentage sizing inside a grid item needs a definite track size;
place-selfbeats margin hacks for pushing a single item within its cell.
Subgrid, briefly, because it changes component design
.card { display: grid; grid-template-rows: subgrid; grid-row: span 3; }
Subgrid lets a child borrow the parent's track lines, so card titles and footers across a row align without fixed heights or min-block-size guesses. Before subgrid the only honest answer was "use a table". It is supported across current engines; if you must support older Safari, align-items: start plus a deliberate type scale is the fallback.
When neither applies
Multi-column text (columns: 2; column-gap: 2rem) for newspaper flow; position: sticky for a scrolling sidebar; a display: inline-flex for a badge that must sit on a text baseline without a wrapper. And if the layout is really "flowing text with a few floats", display: block with margin-inline: auto and a max-inline-size remains the correct answer.
Related: cheat sheet for the property tables, patterns and gotchas for the stacking and containment traps these tools create.