Concept
CSS Grid is a two-dimensional layout system, unlike flexbox (one axis at a time), Grid controls rows and columns simultaneously, with items placeable anywhere in that grid by line number, line name, or named area. This makes it the right tool for actual page-level or component-level layouts (a dashboard, a photo gallery, a page with header/sidebar/main/footer) where flexbox would require nested wrapper divs to fake two dimensions.
Defining a grid
.container {
display: grid;
grid-template-columns: 200px 1fr 1fr; /* 3 columns: fixed, then two flexible */
grid-template-rows: auto 1fr auto; /* 3 rows: content-sized, flexible, content-sized */
gap: 1rem;
}fr (fraction unit) is Grid's most important unit, it represents a share of the remaining space after fixed-size tracks are accounted for. 1fr 1fr splits remaining space equally; 1fr 2fr gives the second column twice the first's share.
repeat() and minmax(), the responsive grid workhorse
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}This single line creates a fully responsive gallery with zero media queries: as many columns as fit, each at least 200px, growing to fill leftover space equally. auto-fill creates as many tracks as fit (leaving empty tracks if there's extra space); auto-fit collapses empty tracks so existing items stretch to fill the row instead, the difference only matters when there are fewer items than would fill a row.
Placing items explicitly
.item {
grid-column: 1 / 3; /* start at line 1, end at line 3 (spans 2 columns) */
grid-row: 2 / 4;
/* shorthand: grid-column: 1 / span 2; is equivalent to 1 / 3 */
}Grid lines are numbered starting at 1 (not the tracks/cells, the lines between and around them). 1 / 3 means "from line 1 to line 3," which spans across the first two column tracks.
Named grid areas, the most readable Grid pattern
.layout {
display: grid;
grid-template-columns: 200px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"sidebar header"
"sidebar main"
"sidebar footer";
}
.sidebar { grid-area: sidebar; }
.header { grid-area: header; }
.main { grid-area: main; }
.footer { grid-area: footer; }grid-template-areas lets you describe a layout as an actual ASCII picture of the page, genuinely more readable than reasoning about numbered lines for anything beyond a simple grid, and it's self-documenting in the CSS file itself.
Implicit vs. explicit grid
.container {
display: grid;
grid-template-columns: repeat(3, 1fr); /* explicit: exactly 3 columns defined */
grid-auto-rows: 150px; /* implicit: any rows created beyond what's explicitly defined get this size */
grid-auto-flow: row | column | dense; /* how auto-placed items fill the grid */
}If you have more items than defined grid cells, Grid creates additional implicit tracks automatically. grid-auto-rows/grid-auto-columns control the size of those auto-generated tracks; grid-auto-flow: dense backfills gaps left by differently-sized items (useful for masonry-like layouts) at the cost of potentially reordering visual placement away from source order.
Subgrid, inheriting the parent's tracks
.parent {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
.child {
display: grid;
grid-column: 1 / 4;
grid-template-columns: subgrid; /* inherits the parent's actual column tracks, not new independent ones */
}Before subgrid (now broadly supported), a nested grid had no way to align its own tracks with its parent's tracks, two independent grids next to each other could never have their columns line up unless you duplicated the exact same track definitions manually. subgrid solves the classic "cards in a row need their internal elements to align across cards, even though each card is layout-independent" problem properly, covered further in the Modern CSS topic.
Grid vs. Flexbox, deciding
| Flexbox | Grid | |
|---|---|---|
| Dimensions | One axis at a time | Two axes simultaneously |
| Best for | Component-internal layout, distributing items in a line | Page/section-level layout, precise 2D placement |
| Content-driven sizing | Very natural | Possible but less the default mode |
| Overlapping items | Awkward | Natural (items can share grid cells) |
A useful heuristic: "do I care about alignment across both rows and columns at once?", if yes, Grid. If you're just distributing items along one line and wrapping, Flexbox is usually simpler and sufficient. They're frequently combined: Grid for the page skeleton, Flexbox inside individual grid cells/components.
Common Mistakes
1. Reaching for nested flexbox to fake two-dimensional layout
/* Awkward: flexbox rows of flexbox columns, fighting to align across rows */
.row { display: flex; }
.row > * { flex: 1; }If columns need to align consistently across multiple rows (a real grid, not just wrapped items), this is exactly what Grid is for, nested flexbox can approximate it but fights you on alignment the moment content sizes differ between rows.
2. Off-by-one errors with grid line numbers
/* Wrong: intending to span 2 columns but only spans 1 */
.item { grid-column: 1 / 2; } /* this is ONE column track, not two */
/* Right */
.item { grid-column: 1 / 3; } /* or: grid-column: 1 / span 2; */Grid lines are the boundaries, not the tracks, a common source of off-by-one confusion, especially coming from zero-indexed thinking. span syntax (1 / span 2) avoids having to calculate the end line manually and is usually clearer intent.
3. Forgetting minmax() in repeat(auto-fit/auto-fill, ...), causing items to overflow or never wrap
/* Wrong: 1fr alone has no minimum, items can shrink to nothing before wrapping ever happens */
grid-template-columns: repeat(auto-fill, 1fr);
/* Right */
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));Without a minmax() minimum, repeat(auto-fill, 1fr) just creates a single column that never wraps, since 1fr has no defined minimum width for the browser to decide "this doesn't fit, wrap to a new row."
4. Confusing auto-fill and auto-fit and being surprised by extra whitespace or stretched items
auto-fill keeps empty tracks (visible as gaps if there are fewer items than would fill a row); auto-fit collapses them, letting existing items stretch into the freed space. Test with a small number of items specifically, this is where the difference actually shows up, they're identical when there are enough items to fill every row.
5. Using grid-template-areas with area names that don't form a valid rectangle
/* Invalid, "header" doesn't form a rectangle */
grid-template-areas:
"header header"
"sidebar header";Every named area must form a complete rectangle across the grid, the browser will reject/ignore an invalid grid-template-areas declaration if the shapes don't tile into rectangles correctly.
Best Practices
- Grid for two-dimensional/page-level layout, Flexbox for one-dimensional/component-internal layout, and combine them freely, they're not mutually exclusive.
repeat(auto-fill, minmax(Npx, 1fr))as the default pattern for responsive card/gallery grids without media queries.grid-template-areasfor any layout complex enough that numbered-line placement gets hard to read, the ASCII-art readability is worth it.- Use
spansyntax (1 / span 2) over manually calculated end lines to avoid off-by-one errors. - Reach for
subgridwhen nested components need their internal tracks to align with an ancestor grid's tracks, rather than duplicating track definitions. - (, ) for complex layouts referenced from multiple places, improving readability over bare numbers.
Further Resources
- MDN, CSS Grid Layout
- CSS-Tricks, A Complete Guide to Grid
- Grid Garden, interactive game for learning Grid.
- web.dev, Learn CSS: Grid
- Rachel Andrew, Grid by Example, patterns from a CSS Grid spec co-editor.
- MDN, Subgrid
