Grid: Rows and Columns Together
Grid is the only layout system on the web that works in two dimensions at once. You describe the tracks, then place things into them - and with named areas you can draw the layout in the stylesheet and read it back a year later.
Defining the tracks
.grid {
display: grid;
grid-template-columns: 200px 1fr 200px;
grid-template-rows: auto 1fr auto;
gap: 1rem;
}
Three columns and three rows. Any length works, and so do three things that are specific to grid.
fr, which is the unit that makes grid worth using
1fr is one share of the leftover space, after fixed tracks and gaps are subtracted.
grid-template-columns: 200px 1fr 1fr; /* 200px, then the rest split evenly */
grid-template-columns: 2fr 1fr; /* two thirds, one third */
This is not the same as a percentage. 50% is half the container including the gap, so 50% 50% with a gap overflows. 1fr 1fr accounts for the gap automatically, which is why fr is almost always the right choice.
repeat, minmax and the responsive grid with no media query
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
That second line is the most useful declaration in modern CSS. Read it as: fit as many columns as you can, each at least 16rem, sharing the leftover space equally.
On a 1200-pixel screen that is four columns. On a phone it is one. There is no breakpoint, nothing to maintain, and it adapts to the container rather than the viewport.
The difference between auto-fill and auto-fit only shows when there are fewer items than fit:
auto-fillkeeps the empty tracks. Three cards in a four-column grid stay card-sized, with a gap on the right.auto-fitcollapses the empty tracks, so three cards stretch to fill the whole row.
Neither is right in general. auto-fit for a gallery that should always look full; auto-fill when a consistent card width matters more.
Placing items
By line number
Grid lines are numbered from 1, and negative numbers count from the end.
.item {
grid-column: 1 / 3; /* from line 1 to line 3: two columns */
grid-row: 2 / 4;
grid-column: 1 / -1; /* full width, however many columns there are */
grid-column: span 2; /* two tracks, wherever it lands */
}
grid-column: 1 / -1 is the one to remember: it means full width without you having to know the column count.
By named area, which is the readable way
.page {
display: grid;
grid-template-columns: 16rem 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"sidebar header"
"sidebar main"
"sidebar footer";
min-height: 100dvh;
gap: 1rem;
}
.page > header { grid-area: header; }
.page > aside { grid-area: sidebar; }
.page > main { grid-area: main; }
.page > footer { grid-area: footer; }
The template is a picture of the layout. Somebody reading this file six months later can see the page shape without opening the browser, which is not true of any other layout technique.
Rearranging for a phone is then one block:
@media (max-width: 48rem) {
.page {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
}
}
No element moved. No element's own CSS changed. A dot is an empty cell:
grid-template-areas:
"header header"
"main ."
"footer footer";
Alignment in both directions
Grid has the same alignment vocabulary as flexbox, and here both axes are available at once:
.grid {
justify-items: center; /* each item, along the inline (row) axis */
align-items: center; /* each item, along the block (column) axis */
place-items: center; /* both, in one line */
justify-content: center; /* the whole grid, when it is narrower than its container */
align-content: center;
place-content: center;
}
.item {
justify-self: end; /* one item, overriding the above */
align-self: start;
place-self: center;
}
place-items: center on a grid container is the shortest true centring in CSS:
.hero { display: grid; place-items: center; min-height: 100dvh; }
Implicit tracks
Place more items than you defined tracks for and grid creates more. grid-auto-rows says how big those are:
.feed {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
grid-auto-rows: minmax(12rem, auto);
gap: 1rem;
}
grid-auto-flow: dense lets grid backfill holes left by items that span several tracks. It does so by taking items out of order visually, which creates the same tab-order mismatch as flexbox order - so use it for a gallery of equivalent things, not for anything sequential.
subgrid, for lining up across cards
A long-standing frustration: three cards in a row, each with a title, body and footer of different lengths, and the footers do not line up because each card is its own formatting context.
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid; /* adopt the parent's rows */
}
Each card now uses the parent grid's row tracks rather than its own, so every title row is the height of the tallest title and every footer starts at the same line. This genuinely was not possible before subgrid without JavaScript measuring things.
A page you can run
A whole page layout by named area, with a card grid inside it that reflows on its own.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A page in two dimensions</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body {
font: 16px/1.6 system-ui, sans-serif;
margin: 0;
background: #fbfbfd;
color: #16181d;
}
/* The layout, drawn. Every area is named once in the template and
once on the element, and the shape of the page is readable from
the stylesheet without running it. */
.page {
display: grid;
min-height: 100vh;
gap: 1rem;
padding: 1rem;
grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
}
/* The one place a breakpoint earns its keep: the structure
genuinely changes, rather than just wrapping. */
@media (width < 48rem) {
.page {
grid-template-columns: minmax(0, 1fr);
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
}
}
.page > header { grid-area: header; }
.page > nav { grid-area: sidebar; }
.page > main { grid-area: main; }
.page > footer { grid-area: footer; }
header, nav, main, footer {
background: #fff;
border: 1px solid #e3e5ea;
border-radius: 10px;
padding: 1rem 1.25rem;
}
nav ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.5rem; }
/* The inner grid: as many columns as fit, each at least 14rem,
and min() so it survives a container narrower than that. */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
gap: 1rem;
}
.card {
border: 1px solid #e3e5ea;
border-radius: 8px;
padding: 1rem;
/* Each card is its own little grid, so the button lines up
across the row however long the text is. */
display: grid;
grid-template-rows: auto 1fr auto;
gap: 0.5rem;
}
.card h3 { margin: 0; font-size: 1rem; }
.card p { margin: 0; color: #5c626e; font-size: 0.9375rem; }
.card button { font: inherit; padding: 0.4rem 0.75rem; border-radius: 6px;
border: 1px solid #0f766e; background: #0f766e; color: #fff; }
</style>
</head>
<body>
<div class="page">
<header><strong>Omni Academy</strong></header>
<nav>
<ul>
<li><a href="#">Courses</a></li>
<li><a href="#">Paths</a></li>
<li><a href="#">Account</a></li>
</ul>
</nav>
<main>
<h1>Named areas</h1>
<p>Drag the window below 48rem and the sidebar moves under the content.</p>
<div class="cards">
<article class="card">
<h3>Short</h3>
<p>One line.</p>
<button type="button">Open</button>
</article>
<article class="card">
<h3>Longer</h3>
<p>Several lines of description, enough to make this card taller than its neighbour if the rows were not stretching together.</p>
<button type="button">Open</button>
</article>
<article class="card">
<h3>Middling</h3>
<p>Two lines or so of text here.</p>
<button type="button">Open</button>
</article>
</div>
</main>
<footer>One grid, three areas, no positioning.</footer>
</div>
</body>
</html>
Open this example in the editor and change it. Nothing you type is saved.
Two separate grids are at work and it is worth separating them in your head. The outer one is a page skeleton: fixed areas, named, with one media query where the structure genuinely changes. The inner one is content-driven: nobody said how many columns, only how narrow a column may get.
Look at the buttons across the card row. They line up even though the descriptions differ in length, because each card is a three-row grid whose middle row takes the slack. That is the cheap version of the trick - subgrid, below, is the thorough one.
Grid or Flex
- Grid when the layout is a shape you can draw: a page skeleton, a card gallery, a form of labels and fields, a dashboard. Use it when things must line up in both directions.
- Flex when the content decides: a row of buttons, a toolbar, the inside of a card, anything that should wrap naturally.
They compose. A grid for the page, flex inside each region, is the normal arrangement rather than a compromise.
What goes wrong
| Symptom | Cause | Fix |
|---|---|---|
| Columns overflow the container | 50% 50% plus a gap |
Use 1fr 1fr |
| A long word stretches a track | Grid item min-width: auto |
minmax(0, 1fr) |
| Cards do not fill the last row | auto-fill |
auto-fit |
| Named areas do nothing | The template is not rectangular | Every row must have the same number of columns |
| An item ignores its placement | A typo in the area name - CSS fails silently | Check the template against the grid-area values |
| Nothing lines up between cards | Each card is its own grid | subgrid |
minmax(0, 1fr) deserves the same emphasis as min-width: 0 in flexbox: 1fr means minmax(auto, 1fr), and that auto minimum is what lets content push a track wider than its share. If a grid column is refusing to shrink, this is why.
Use the inspector
Both Firefox and Chrome draw the grid over the page - every line, numbered, with the area names and the track sizes labelled. Turn it on from the Layout panel the first time a grid does something you did not expect; it answers in seconds what reading the CSS answers in twenty minutes.