Learn CSS · Lesson 7 of 11 · 12 min
CSS Grid
Widely availablesince Apr 2020 Widely availablesince Mar 2026
Flexbox arranges one line. Grid arranges rows and columns at the same time, so things line up in both directions. It looks like a lot of syntax, and most of it turns out to be six nouns. Learn the nouns and the properties read like sentences.
Six words
Line
The dividers, numbered from 1. Three columns have four column lines. Items are placed between lines, and -1 always means the last one.
Track
The space between two neighbouring lines: a whole column or a whole row. grid-template-columns is a list of track sizes.
Cell
One column crossing one row. The smallest unit, and what an item occupies when you give it no instructions.
Area
A rectangle of cells. Any item can cover an area, and an area can be given a name.
Gap
The gutter between tracks. It exists only between them, never around the outside of the grid.
Implicit track
A row or column you never declared, which the grid adds because there were more items than cells. Most rows on the web are implicit.
Draw a layout, get the CSS
Pick a name, then click or drag across the cells to paint it. The strings underneath are what
grid-template-areas takes, and the preview on the right is laid out by them.
On the container
six cards · they go on the parentColumns that share the space
.layout {
display: grid;
grid-template-columns: 200px 1fr 2fr;
} Goes on: the container.
One value per column. fr is a share of what is left after the fixed tracks and the gaps are taken out, so here the last column is twice the middle one and the first never changes.
What it replaced: percentages that had to add up to 100 minus the gutters, worked out by hand.
As many columns as fit
.cards {
display: grid;
grid-template-columns:
repeat(auto-fit, minmax(min(100%, 140px), 1fr));
} Goes on: the container.
Fit as many 140px columns as the container allows, then stretch them to fill the row. The column count changes by itself as the container changes, with no media query. The min(100%, …) keeps a single column from overflowing a container narrower than 140px. Drag the corner of the figure.
What it replaced: three or four breakpoints, each resetting a width and an nth-child margin.
Gutters
.cards {
gap: 24px 12px; /* rows, then columns */
} Goes on: the container.
The same property as in flexbox. Two values set the row gap and the column gap separately.
Name the regions
.page {
display: grid;
grid-template-columns: 180px 1fr;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
} Goes on: the container.
Each string is a row and each word is a cell. Repeating a name makes the area bigger, and a full stop leaves a cell empty. Every area has to be a rectangle. If one is not, the browser throws away the whole declaration. The widget above writes this for you.
Size the rows you never declared
.cards {
grid-auto-rows: minmax(90px, auto);
} Goes on: the container.
Items past the end of your template land in implicit rows, which are sized to their content unless you say otherwise. This gives every such row a floor of 90px and still lets it grow for long content.
What it replaced: a min-height on every card.
Align what is inside every cell
.icons {
display: grid;
place-items: center;
} Goes on: the container.
Shorthand for align-items and justify-items. Items are stretched to fill their cell unless told otherwise. This centres each one in its own cell, both ways. On a grid with a single cell it is the shortest way to centre anything.
On an item
four cards · they go on a childCover more than one cell
.feature {
grid-column: span 2;
grid-row: span 2;
} Goes on: the item.
span counts tracks from wherever the item lands. The other items flow around it, and you did not have to touch them.
What it replaced: a table with colspan and rowspan.
Place by line number
.banner {
grid-column: 1 / -1;
} Goes on: the item.
From the first column line to the last. Negative numbers count from the end, so this is full width however many columns the grid has at the moment. It only counts columns you declared, not implicit ones.
Drop an item into a named area
.site-header {
grid-area: header;
} Goes on: the item.
The other half of card four. The HTML order no longer decides where a region sits, so one media query that rewrites the template strings can rearrange the whole page.
Careful. As with order in flexbox, this moves pixels only. Keep the HTML in the order it should be read.
Line up the insides of separate cards
Widely availableA grid only aligns its direct children. Three pricing cards are three separate boxes, so a
long title in one pushes its button lower than its neighbours’. With subgrid a card
borrows its parent’s row lines for its own children, and titles, text and buttons line up across all
three cards.
Starter
One short line.
ChooseA much longer plan name that wraps
This description runs to several lines, which used to push this card’s button out of line with its neighbours.
ChooseTeam
Two lines of text here, a little more than the first.
Choose.cards { display: grid; grid-template-columns: repeat(3, 1fr); }
.card { display: grid; grid-row: span 3; grid-template-rows: subgrid; } Your browser does not support this yet, so the demo above shows the fallback. Without subgrid each card sizes its own rows, so titles and buttons stop lining up across cards. The content is all still there. Wrap the subgrid lines in @supports (grid-template-rows: subgrid) if the unaligned version needs different spacing.
Goes on: the item, which must itself be a grid.
What about masonry?
A layout where items pack upwards into the shortest column, like a wall of pins. CSS has a draft for it, and the browser dataset this site reads at build time has no entry for it at all, which is an answer in itself: do not plan a layout around it yet. Use card two, which gives an even wall with aligned rows, or CSS columns if only the vertical packing matters and reading order does not. When the dataset gains an entry, this paragraph will change by itself.
Practice
- In the widget above, paint a layout where
asidesits to the right ofmainand the footer is full width. Then break it on purpose: makemainan L shape and read the message. - Open the Bento Grid Builder, export a layout, and find cards one, three and seven in the CSS it gives you.
- Open DevTools on any grid and click the
gridbadge in the Elements panel. The browser draws every line with its number, positive and negative. Count them against the first of the six words.
Check yourself
A grid has four columns. How many column lines does it have, and what number is the last one?
Five. The last is line 5, and it is also line -1, which is why
grid-column: 1 / -1 keeps meaning “full width” when the column count changes.
You wrote grid-template-rows for two rows and added seven items to a three-column grid. What sizes the third row?
Nothing you wrote. It is an implicit row, sized to its content, unless you set
grid-auto-rows (card five).
Your grid-template-areas is being ignored completely. What is the likeliest cause?
One of the names does not form a rectangle, or the rows have different numbers of cells. Either makes the whole declaration invalid, and the browser drops it without a message. DevTools shows it struck through.
The mental model to keep
The container draws lines. Items sit between lines. Everything else, fr, repeat, minmax, areas, span, is a shorter way of saying where the lines are or which ones an item sits between. Use grid for the skeleton of a page and anything that must align in two directions, and flexbox inside the cells for the single rows of things. Next: centering, decided.
Last reviewed 2026-09-18. Browser status on this page is read from the web-features dataset when the site is built, not typed. The page prints as its cards.