CSS Grid — The Complete Guide
Two-dimensional layout done right — grid-template-columns/rows, grid areas, and the mental model that makes Grid click.
display: grid — Thinking in Rows AND Columns at Once
Every layout tool you have used so far in this track — normal document flow, floats, Flexbox — is fundamentally one-dimensional. Flexbox lays items out along a single axis (a row, or a column) and lets the cross axis take care of itself, item by item. CSS Grid is different in one specific, load-bearing way: it lets you define rows and columns at the same time, as a single coordinate system, and then place content anywhere inside that grid — including deliberately out of source order. That is the entire idea Grid exists to solve. If you have ever tried to build a page layout in Flexbox and found yourself fighting to get a sidebar, header, and footer to all line up against a shared set of column and row boundaries, that fight is exactly what Grid was built to end.
.layout {
display: grid;
grid-template-columns: 200px 1fr 200px;
grid-template-rows: 80px 1fr 60px;
gap: 16px;
}Setting display: grid on an element makes it a grid container — every direct child automatically becomes a grid item, without needing any property set on the children themselves (unlike Flexbox, where you at least conceptually think about each child's flex behaviour). grid-template-columns and grid-template-rows define the actual track sizes: the layout above creates three columns (200px, a flexible middle column, 200px) and three rows (80px, a flexible middle row, 60px) — a skeleton that reads almost exactly like a hand-drawn wireframe.
<div class="layout">
<header>Header</header>
<main>Main content</main>
<aside>Sidebar</aside>
</div>By default, grid items are placed automatically into the grid, one per cell, in source order, filling row by row — the same instinctive "just drop them in" behaviour you get from normal block flow, except now flowing across a real two-dimensional grid instead of a single top-to-bottom column. Explicit placement (Part 05) is what lets you break out of that automatic order deliberately.
grid-gap, which is now just a legacy alias) puts real space between grid tracks without adding margin to individual items — meaning there is no lingering space at the outer edges of the grid the way margin-based spacing between Flexbox items always leaves behind. You can also set row-gap and column-gap independently if the spacing needs differ between the two axes.inline-grid — the rarely-needed sibling
Just as Flexbox has inline-flex, Grid has display: inline-grid — the grid container itself behaves like an inline-level box in the surrounding layout, while everything inside it still lays out as a grid. This is genuinely rare in practice; the vast majority of real Grid usage is display: grid on a block-level container.
fr — The Unit That Only Exists for Grid
fr stands for "fraction" — a unit that represents a share of the leftover space in the grid container, after every fixed-size track (pixels, rems, percentages) has already been subtracted. It is the single idea that makes Grid track sizing feel effortless once it clicks, and genuinely confusing before it does.
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
/* Three equal columns, each getting exactly one third of the available width.
There is no leftover space to distribute unevenly — all three "shares" are 1. */.layout {
display: grid;
grid-template-columns: 250px 1fr;
/* The sidebar is a fixed 250px, no matter the container width.
The 1fr column gets EVERYTHING that's left over — not "the rest divided by 1",
but literally "100% of whatever remains after 250px is subtracted." */
}The genuinely important detail: fr distributes space proportionally among fr tracks only, after fixed tracks are already accounted for. grid-template-columns: 1fr 2fr 1fr does not mean "25%, 50%, 25% of the container" in the way percentages would — it means "whatever space is left after gaps and fixed tracks, split it 1 part : 2 parts : 1 part." The middle column ends up exactly twice as wide as each outer one, but the actual pixel values shift as the container resizes.
/* Percentages: technically works, but gap has to be manually subtracted
from somewhere, or the columns overflow the container. */
.old-way {
display: grid;
grid-template-columns: 33.33% 33.33% 33.33%;
gap: 16px; /* this gap is NOT accounted for in the 33.33% figures — overflow risk */
}
/* fr: gap is subtracted automatically before the fr units are calculated.
No manual math, no overflow. */
.grid-way {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
}grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) instead of a bare 1fr 1fr. A bare fr track has an implicit minimum width based on its content (min-width: auto), which means a long unbreakable string or a wide image inside a grid item can force that track wider than its fair fr share — wrapping it in minmax(0, 1fr) overrides that implicit minimum and lets the track actually shrink to its fr share. This exact fix — "my grid column won't shrink even though I gave it 1fr" — is one of the most searched CSS Grid problems that exists.repeat() and minmax() — Writing Less, Describing More
Writing out 1fr 1fr 1fr 1fr 1fr 1fr for a six-column grid works, but it does not scale, and it does not communicate intent. repeat() lets you express "N tracks of this size" directly.
.grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
/* Identical to: 1fr 1fr 1fr 1fr 1fr 1fr */
}
.grid-mixed {
display: grid;
grid-template-columns: 200px repeat(3, 1fr) 100px;
/* repeat() can appear alongside other track definitions, not just alone */
}minmax(min, max) defines a track that is never smaller than its minimum and never larger than its maximum — genuinely flexible sizing within real, explicit bounds, rather than either a fixed size or unlimited growth.
.card-grid {
display: grid;
grid-template-columns: repeat(3, minmax(200px, 1fr));
/* Each column is at least 200px, but grows to share the leftover space
equally (1fr) if the container is wide enough to give it more than 200px. */
}Combined, repeat() and minmax() produce the single most useful line in all of CSS Grid — the pattern that builds a genuinely responsive grid with zero media queries.
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 20px;
}
/* Reads as: "Fit as many 220px-minimum columns as will comfortably fit,
then let each one grow to fill any leftover space." As the viewport
shrinks, columns wrap down automatically — no breakpoint needed. */This deserves its own full treatment — the difference between auto-fill and auto-fit, and a real image gallery built on exactly this pattern, is covered in depth in the next module, CSS Grid in Practice. For now, the important thing to internalise is that repeat() accepts auto-fill/auto-fit as its count argument instead of a fixed number — that is what makes the column count itself responsive, not just the column widths.
minmax(200px, 1fr) is very different from minmax(1fr, 200px) — the arguments are not interchangeable. The first argument must always be the smaller bound and the second the larger; reversing them (a fr unit as the minimum, a pixel value as the maximum) is invalid and the browser drops the entire declaration.grid-template-areas — Drawing Your Layout in the CSS Itself
grid-template-areas is genuinely unlike anything else in CSS: you name regions of the grid, then draw the layout as an actual ASCII-art-style grid of those names directly in your stylesheet. It reads like a wireframe because it effectively is one.
.layout {
display: grid;
grid-template-columns: 220px 1fr;
grid-template-rows: 70px 1fr 60px;
grid-template-areas:
"sidebar header"
"sidebar main"
"sidebar footer";
}
header { grid-area: header; }
main { grid-area: main; }
footer { grid-area: footer; }
aside { grid-area: sidebar; }Each child is assigned to a named region with grid-area, and the container's grid-template-areas string literally draws where each region sits — the sidebar spans all three rows in the example above simply because "sidebar" appears in every row of the drawing. There is no separate row/column-span property needed for this case; the shape of the ASCII drawing itself is the spanning logic.
. (a period) for a cell that is deliberately empty — an intentional gap in the grid that no item occupies..layout {
grid-template-columns: 220px 1fr 1fr;
grid-template-areas:
"sidebar header header"
"sidebar . widget"
"sidebar footer footer";
}
/* The middle cell in row 2 is deliberately empty — no item is placed there. */Why this earns its place over grid-column/grid-row for real page layouts
You could achieve an identical result with numeric line placement (Part 05) — but grid-template-areas is dramatically more readable for anyone maintaining the layout later, since the CSS visually mirrors the actual page structure. This is why it is the dominant pattern for full-page, semantically distinct layouts (header/nav/main/aside/footer), while numeric line placement tends to be reached for inside smaller, more repetitive components like card grids, where naming every cell would be excessive ceremony for little benefit.
grid-column / grid-row — Placing Items by Line Number
Every grid has numbered grid lines — not tracks, the lines between tracks. A 3-column grid has 4 vertical grid lines (numbered 1 through 4, left to right); a 3-row grid has 4 horizontal grid lines. grid-column and grid-row place an item by specifying which lines it starts and ends at — this is the mechanism that lets an item span multiple tracks, or be placed somewhere other than the browser's automatic next-available cell.
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(2, 100px);
gap: 12px;
}
.hero {
grid-column: 1 / 3; /* start at line 1, end at line 3 — spans 2 columns */
grid-row: 1 / 2;
}
.wide-banner {
grid-column: 1 / -1; /* -1 always means "the last line" — spans the FULL width,
regardless of how many columns the grid actually has */
}grid-column: 1 / 3 is shorthand for grid-column-start: 1; and grid-column-end: 3; — the same relationship margin has to margin-top/right/bottom/left. The end line is exclusive of nothing in particular — it is just "the line the item's edge touches," so an item spanning from line 1 to line 3 covers exactly two column tracks (the space between lines 1–2 and 2–3).
.card {
grid-column: span 2; /* "start wherever the auto-placement algorithm puts me,
but occupy 2 columns from there" — no explicit start line needed */
}span N is genuinely the more common real-world pattern for grids where items still flow automatically but occasionally need to be wider — a "featured" card in an otherwise uniform product grid, for example — since it does not require calculating exact line numbers by hand, which becomes fragile the moment the column count changes.
-1 is always the last line, -2 is one before that, and so on. This is what makes grid-column: 1 / -1 a genuinely robust "always span the full width of the grid" pattern — it works correctly even if the number of columns changes later, unlike hardcoding an end line number that assumes a specific column count.The Explicit Grid You Define, and the Implicit Grid the Browser Invents
The explicit grid is exactly the tracks you defined with grid-template-columns and grid-template-rows. The implicit grid is what the browser silently creates when content needs more rows or columns than you explicitly defined — a detail that catches nearly every developer off guard the first time it happens.
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 100px 100px; /* only 2 rows explicitly defined */
}
/* 9 items, 3 columns → 3 rows are needed, but only 2 were defined.
The browser creates a 3rd row IMPLICITLY, and — critically — that
implicit row does NOT get the 100px height from grid-template-rows.
By default, it sizes to fit its content instead. */This is the single most common source of "why is my last row a different height than the others" bugs in real Grid layouts. The fix is grid-auto-rows (and its column equivalent, grid-auto-columns), which sets the size for any track the browser creates implicitly — the counterpart to grid-template-rows, but for rows you did not explicitly name.
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 100px 100px;
grid-auto-rows: 100px; /* any implicit row created beyond the two explicit
ones is ALSO 100px, matching the rest of the grid */
}grid-auto-flow — controlling how items fill the implicit grid
By default (grid-auto-flow: row), auto-placed items fill row by row, creating new rows as needed. Setting it to column flips that — items fill column by column instead, creating new columns as needed (which requires grid-auto-columns to size those new columns sensibly).
.grid {
display: grid;
grid-template-rows: repeat(2, 100px);
grid-auto-flow: column;
grid-auto-columns: 150px;
}
/* Items fill DOWN each column first, then move to the next column —
the opposite of the default row-first behaviour. */grid-auto-flow: dense is a lesser-known modifier worth knowing: it tells the auto-placement algorithm to backfill earlier empty cells with later items that happen to fit, rather than strictly preserving source order. It is genuinely useful for masonry-style grids with items of mixed spans, where leaving gaps would otherwise look broken — but it comes at the direct cost of visual order no longer matching source (DOM) order, which can be a real accessibility concern for keyboard and screen-reader navigation. Use it deliberately, not by default.justify-items, align-items, and Their content Counterparts
Grid alignment properties split cleanly along the same two axes the rest of Grid is built on, and along a second distinction that trips people up at first: aligning items within their own cell versus aligning the whole grid within its container, when the grid itself is smaller than the container.
.grid {
display: grid;
justify-items: center; /* horizontal alignment of each item inside its own cell */
align-items: center; /* vertical alignment of each item inside its own cell */
}
/* Shorthand for both at once: */
.grid {
place-items: center; /* align-items then justify-items, in that order */
}.grid {
display: grid;
grid-template-columns: repeat(3, 100px); /* a grid narrower than its container */
justify-content: center; /* centers the WHOLE grid horizontally in the container */
align-content: center; /* centers the WHOLE grid vertically in the container */
}The naming mirrors Flexbox closely on purpose — justify-* is always the inline (typically horizontal) axis and align-* is always the block (typically vertical) axis in both layout modes. The -items vs -content distinction is the part that is genuinely specific to Grid: -items properties move things inside their own cell; -content properties move the entire set of tracksas a group within the container, and only have a visible effect when the defined tracks do not already fill the container completely.
A single item can also override the container's justify-items/ align-items for itself specifically, using justify-self and align-self — exactly the same "container sets the default, an individual item can opt out" relationship align-self already has in Flexbox.
.grid { justify-items: start; }
.featured-card {
justify-self: center; /* this one item centers itself, ignoring the container's "start" */
}A Design-System Rebuild at a Portland Analytics Company
A front-end engineer at a Portland-based analytics company inherits a dashboard shell built years earlier from floated divs and a handful of absolutely-positioned overrides for the sidebar. Every time a designer requests a layout tweak — widening the sidebar, adding a secondary top bar for filters — the change requires touching three or four unrelated CSS rules, and something else regressed almost every time.
.shell {
position: relative;
}
.sidebar {
position: absolute;
top: 0; left: 0; bottom: 0;
width: 240px;
}
.topbar {
margin-left: 240px;
height: 60px;
}
.content {
margin-left: 240px;
padding-top: 60px;
min-height: 100vh;
}
.footer {
margin-left: 240px;
}
/* Every region's position depends on hardcoded knowledge of every OTHER
region's size, duplicated across four separate rules. Change the sidebar
width once, and it has to be updated in three unrelated places. */The rebuild
The engineer replaces the entire shell with a single grid-template-areas declaration — the exact pattern from Part 04 of this module. The sidebar width now lives in exactly one place.
.shell {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: 60px 1fr auto;
grid-template-areas:
"sidebar topbar"
"sidebar content"
"sidebar footer";
min-height: 100vh;
}
.sidebar { grid-area: sidebar; }
.topbar { grid-area: topbar; }
.content { grid-area: content; }
.footer { grid-area: footer; }Resizing the sidebar is now a single-number change to grid-template-columns, with zero risk of a stray region silently overlapping another — every region's boundary is defined by the grid itself, not by each region independently guessing the sidebar's width. When a request comes in a week later to add a collapsible sidebar state, the fix is a single class toggle that changes grid-template-columns from 240px 1fr to 60px 1fr, with a transition — no repositioning logic needed anywhere else, because everything downstream of the sidebar already reflows automatically off the grid definition.
Five Misconceptions About CSS Grid
6 Interview Questions — With Complete Answers
CSS Grid Mistakes Beginners Make Constantly
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.item { width: 400px; } /* fighting the grid track size directly */.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
/* Don't set a fixed width on a grid ITEM to control its track size —
size the TRACK itself in grid-template-columns instead. */.layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main";
}
main { grid-area: content; } /* typo — "content" was never defined */.layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main";
}
main { grid-area: main; } /* matches the name used in grid-template-areas exactly */.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.item {
grid-column: 1 / 4; /* meant to span 3 columns... */
grid-row: 1;
}.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.item {
grid-column: 1 / 4; /* correct — a 3-column grid has 4 lines (1,2,3,4),
so spanning all 3 columns really is "1 / 4" */
}.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, 220px); /* no minmax() at all */
}.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}Errors and Rendering Bugs You Will Hit With Grid — And Exactly Why
🎯 Key Takeaways
- ✓CSS Grid defines rows and columns simultaneously as a single coordinate system — the defining difference from every one-dimensional layout tool you have used so far, including Flexbox.
- ✓fr distributes leftover space AFTER fixed tracks and gap are subtracted — it is not a percentage, and gap does not need to be manually accounted for the way it does with percentage-based tracks.
- ✓repeat(auto-fill, minmax(min, 1fr)) builds a genuinely responsive grid with zero media queries — it fits as many minimum-sized columns as comfortably fit, then lets them grow to fill any leftover space.
- ✓grid-template-areas lets you literally draw your layout in CSS — every named region must form a single unbroken rectangle, and every row of the ASCII string needs the same cell count, or the whole declaration is invalid.
- ✓grid-column/grid-row place items by line number, not track number — an N-column grid has N+1 lines. span N is usually the more robust choice over exact line numbers for items that should still mostly auto-place.
- ✓The implicit grid is created automatically whenever content needs more tracks than you explicitly defined — those implicit tracks size to their content unless grid-auto-rows/grid-auto-columns say otherwise.
- ✓justify-items/align-items (or place-items) align content WITHIN each cell; justify-content/align-content align the entire grid AS A GROUP within its container — the two only look identical when the grid exactly fills its container.
What comes next
Now that the full Grid vocabulary is in place, the next module puts every piece of it to work on real layouts — a holy grail page shell, a responsive image gallery, and a genuinely non-trivial dashboard.
Next Module → CSS Grid in Practice — Real LayoutsDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.