Flexbox solves a one-dimensional problem. You have a row (or a column) of things and you want to control how they share space along that line. Wrapping exists, but each wrapped line is laid out independently — items on line two have no idea what items on line one are doing, which is why flexbox card layouts never quite line up vertically.
Grid solves a two-dimensional problem. You define rows and columns up front, and the result is a lattice of cells that exists before any content arrives. Items are placed into that structure; they do not push each other around. This is the inversion that trips people up. In flexbox, content shapes the layout. In Grid, the layout is declared first and content conforms to it. It is the first layout system in CSS that was designed for two dimensions rather than bent into them — floats were for wrapping text around images, and flexbox was for distributing space along a line. Once you stop waiting for items to negotiate with each other and start asking where on the surface each thing goes, most of the perceived difficulty disappears.
Grid has exactly two roles: the container and the items. The container declares the structure — display: grid, the column and row tracks, the gap between them. The items, by default, do nothing at all. They flow into the cells left to right, top to bottom, like text filling lines on a page. Compare that with flexbox, where half the interesting properties (flex-grow, flex-shrink, flex-basis) live on the children. With Grid you can build a complete layout without touching a single child selector.
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}<div class="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
</div>On screen: three equal columns and two rows — items 1 to 3 across the top, 4 to 6 below — with a uniform 1rem gutter between everything.
gap deserves a sentence of its own. Before it existed, we faked gutters with margins and then fought the leftover margin on the last item of every row. gap applies spacing only between tracks, never on the outer edges, and it covers both axes at once (or separately, via row-gap and column-gap). No negative-margin hacks, no :last-child resets.
The fr unit means one fraction of the space that is left over, and the order of operations is the whole point. The browser first subtracts every fixed-width track and every gap from the container width, and only then divides what remains among the fr tracks. Percentages do not work this way. A percentage is a fraction of the whole container, blind to everything else in the row — so three 33.33% columns plus two 1rem gaps add up to more than 100% and overflow. Three 1fr columns plus the same gaps fit exactly, at every viewport width, forever.
/* Fixed sidebar, fluid main column */
.layout {
display: grid;
grid-template-columns: 250px 1fr;
gap: 2rem;
min-height: 100dvh;
}
/* Three columns, the middle twice as wide */
.three-col {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
gap: 1.5rem;
}On screen, the first rule produces a 250px sidebar pinned to the left with a main column absorbing every remaining pixel; the second produces three columns where the middle one is exactly twice the width of its neighbours, gaps already accounted for.
That fixed-plus-fluid pair — a pixel value next to a 1fr — is quietly one of the most useful lines in CSS. Sidebars, chat layouts, label-and-input rows, image-and-caption pairs: all the same shape.
This is the line that sells Grid to most people:
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}On screen: a wall of cards — four across on a wide desktop, two on a tablet, a single column on a narrow phone — and you never wrote a media query.
It looks like magic until you read it term by term, at which point it becomes arithmetic.
- repeat() stamps out a track pattern so you do not have to write each track by hand.
- auto-fit says: do not tell me how many tracks — create as many as currently fit across the container.
- minmax(250px, 1fr) is each track's contract: never narrower than 250px, allowed to grow to an equal share of the leftover space.
Put together: the browser asks how many 250px tracks (plus gaps) fit in the current width, creates exactly that many, then lets the 1fr maximum stretch them so the row fills edge to edge. Resize the window and the count is recomputed continuously. The breakpoints are still there — they are just derived from the content's own minimum width instead of being hardcoded, which means they stay correct when the design changes. Change 250px to 300px and every breakpoint moves with it.
The sibling keyword auto-fill behaves almost identically, with one visible difference: when there are fewer items than tracks, auto-fill keeps the empty tracks, so two cards on a wide screen sit at the left at roughly minimum width with reserved space beside them. auto-fit collapses the empty tracks, so the same two cards stretch to share the full row. For card grids, auto-fit is usually what you want; auto-fill is for when items should keep a consistent size regardless of how many exist.
For whole-page scaffolds, named areas are the most readable tool in CSS. You draw the layout as strings — one string per row, one word per column — and then assign each child to a name.
.page {
display: grid;
grid-template-areas:
"header header header"
"sidebar content aside"
"footer footer footer";
grid-template-columns: 200px 1fr 200px;
grid-template-rows: auto 1fr auto;
min-height: 100dvh;
gap: 1rem;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.content { grid-area: content; }
.aside { grid-area: aside; }
.footer { grid-area: footer; }On screen: a classic three-panel page — a full-width header, a 200px sidebar and a 200px aside flanking fluid content, and a footer pinned across the bottom of the viewport even when the content is short.
The ASCII-art quality is not a gimmick; it is the feature. The CSS is a picture of the rendered result, which makes this the rare layout code you can review at a glance. Repeating a name across cells merges them into one region — that is how the header spans all three columns without any line-number bookkeeping.
tip
When you reshuffle a layout, you edit the strings, not the selectors. Moving the sidebar to the right is a one-line text edit, and a mobile variant is just a different set of strings inside a media query. Six months from now you will still understand the structure instantly — something no stack of nested flexbox wrappers can claim.
Sometimes flow order is not enough — a featured card should be larger than its siblings, or an element belongs in one specific cell. Grid lets you pin items to numbered lines, and the detail that matters is that you count lines, not columns. Three columns means four vertical lines, numbered from 1. Negative numbers count from the far end, so 1 / -1 always means full width no matter how many columns the grid has.
.featured {
grid-column: 1 / 3; /* from line 1 to line 3 = two columns */
grid-row: 1 / 3; /* two rows */
}
/* Relative sizing with span */
.wide-item {
grid-column: span 2; /* two columns from wherever it lands */
}
.full-bleed {
grid-column: 1 / -1; /* always the entire row */
}On screen: .featured renders as one large 2-by-2 block in the top-left corner while ordinary items flow around it; .wide-item is double width wherever it happens to land; .full-bleed stretches across the whole row.
warning
If you place an item on a row you never defined — grid-row: 5 in a two-row grid — Grid will not error. It silently creates implicit rows sized auto, which usually means as tall as the content and no taller. The first time a layout sprouts a mysteriously squashed extra row, this is why. Set grid-auto-rows on the container if items might ever land outside the rows you declared.
The choice between Grid and flexbox reduces to one question: am I arranging things along a line, or across a surface? A navigation bar, a row of buttons, a label beside an input, centring one element inside another — those are lines, and flexbox is the lighter tool. A page scaffold, a wall of cards, a form whose fields must align in columns, anything where rows and columns have to agree with each other — those are surfaces, and that is Grid. The two compose naturally rather than competing: Grid positions the card on the page, flexbox pushes the card's footer to its bottom edge. The reliable smell that you picked wrong is nesting — if you are stacking flexbox containers inside flexbox containers to fake alignment across rows, the layout was two-dimensional all along.
Here is everything joined together — an article list with one feature card and a set of small cards, the layout every blog index eventually wants.
.article-list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-auto-rows: minmax(200px, auto);
gap: 1.5rem;
}
.article-list > .feature {
grid-column: span 2;
grid-row: span 2;
}
@media (max-width: 700px) {
.article-list {
grid-template-columns: minmax(0, 1fr);
}
.article-list > .feature {
grid-column: auto;
grid-row: auto;
}
}On screen: a large feature card filling the top-left two-thirds of the grid, small cards wrapping around it down the right side and underneath like a magazine cover, and the whole arrangement collapsing into a single readable column below 700px.
Two honest notes. First, this is the one place in the article where I write a media query, because a spanning item and auto-fit do not mix — on a narrow screen, a card that spans two columns of a one-column grid forces an overflow. Hardcoding the column count and resetting the span at one breakpoint is the boring, correct answer. Second, look at minmax(0, 1fr) in the column definition. That zero is doing real work.
warning
Plain 1fr is shorthand for minmax(auto, 1fr), and auto means a track refuses to shrink below its content's minimum size. One long URL, one wide code block, one unbreakable string inside a card, and your equal columns silently stop being equal — the track grows and the layout overflows. minmax(0, 1fr) on the track, or min-width: 0 on the item, tells the browser the content may wrap or be clipped instead. You will hit this eventually; everyone does.
Strip away the rest of the spec and most production layouts reduce to three shapes:
- The card wall: repeat(auto-fit, minmax(250px, 1fr)) — responsive collections with zero media queries.
- The page scaffold: grid-template-areas — headers, sidebars and footers you can read like a diagram.
- The fixed-plus-fluid pair: 250px 1fr — sidebars, media objects, label-and-field rows.
Learn those three until you can type them without thinking, and reach for explicit line placement only when a design genuinely calls for it. The next step is not more reading. Open a project you have already built, find a page held together with nested flexbox wrappers, and rebuild it on a grid-template-areas scaffold. The CSS will come out shorter, and — more importantly — you will be able to read it back.
