CSSTidy CSS Formatter

Grid vs Flexbox: The Decision, Not the Feature List

CSSTidy — CSS Formatter & Minifier Guides · Updated 2026-10-12 · All guides

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

QuestionUse
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 onlyFlex only
grid-template-areas, named linesflex-grow, flex-shrink, flex-basis
grid-auto-flow: densejustify-content: space-between on the main axis with implicit sizing
subgridorder — exists in grid too, but only behaves sanely in flex
align-content on both axesflex-flow shorthand
place-items / place-contentbaseline 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: auto on 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: 0 plus overflow: auto.
  • flex-direction: column + align-items: stretch does not stretch a child that has an intrinsic size — that is why a nested <svg> in a column flex container ignores width: 100% until you give it align-self: stretch.
  • Wrapping with gap and align-content behaves differently from align-items: align-items positions items within a line, align-content positions the lines. A wrapped flex row that "ignores" align-items: center is almost always fixed by also setting align-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 set grid-auto-flow: row dense deliberately — dense reorders visually and breaks reading order for screen-reader and keyboard users who follow DOM order.
  • 1fr is minmax(auto, 1fr), which is the source of nearly every "my grid item will not shrink" report. minmax(0, 1fr) on the track, or min-width: 0 on the item.
  • Percentage sizing inside a grid item needs a definite track size; place-self beats 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.

Developer Sponsor / Partner
Copied to clipboard!