Grid: rows and columns
In short: CSS Grid lays items out in rows and columns at once: columns with fr and gap, a catalogue with repeat(auto-fill, minmax()) and a page layout with grid-template-areas.
Problem
The last product is huge
The catalogue uses flex-wrap from the last lesson. Four products: three fit on the first line, and the fourth is alone on the second line and as wide as the whole line.
Console
This is not a bug in flexbox. Each line of a flex container shares its own free space and knows nothing about the line above. The second line has one product, so it takes everything: 768px. We need something that draws the columns once for the whole catalogue.
The idea
Rows and columns
Grid is a layout in two dimensions: the container draws columns and rows, and the items go into the cells one by one, row by row.
.catalog { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 24px;}display: gridmakes the element a grid container, and its children are grid items.grid-template-columnssays how many columns there are and how wide each is:200px 1fris a 200px column and a column that takes the rest.fris a share of the free space, after the sized columns and the gaps are taken out.1fr 2fr 1fris 1 : 2 : 1.repeat(3, 1fr)is the same as1fr 1fr 1fr.gapis the space between the columns and between the rows.
The difference from flexbox: with flex the items decide how wide they are, and each line is separate. With grid the container decides the columns, and the items only fill them. So the last product stays in its column.
Try it
Three columns
Make the catalogue a grid with three equal columns and a 24px gap. The catalogue is up to 720px, so every card, the last one too, has to be (720 − 2 × 24) : 3 = 224px.
Console
Solution
index.html<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Catalogue</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Catalogue</h1>
<div class="catalog">
<article class="card">
<img src="img/bread.svg" alt="" width="48" height="48">
<h2>White bread</h2>
<p>€1.20</p>
</article>
<article class="card">
<img src="img/bread.svg" alt="" width="48" height="48">
<h2>Wholegrain bread</h2>
<p>€1.90</p>
</article>
<article class="card">
<img src="img/milk.svg" alt="" width="48" height="48">
<h2>Fresh milk</h2>
<p>€1.60</p>
</article>
<article class="card">
<img src="img/milk.svg" alt="" width="48" height="48">
<h2>Yoghurt</h2>
<p>€1.10</p>
</article>
<article class="card">
<img src="img/cheese.svg" alt="" width="48" height="48">
<h2>White cheese</h2>
<p>€7.90</p>
</article>
</div>
</body>
</html>
style.css* { box-sizing: border-box; }
body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; }
h1 { margin: 0 0 12px; font-size: 1.4rem; }
.catalog {
max-width: 720px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 24px;
}
.card {
flex: 1 1 200px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
}
.card h2 { margin: 8px 0 4px; font-size: 1rem; }
.card p { margin: 0; }
display: grid and grid-template-columns: 1fr 1fr 1fr (or repeat(3, 1fr)). gap: 24px is already there, and with grid it applies between the columns and between the rows. flex: 1 1 200px on the cards does nothing any more: they are not flex items. 1fr 2fr 1fr gives three columns, but the middle one is double.
The idea
As many as fit
Three columns are fine on a computer. On a phone they are 100px each, and the product cannot be read. We do not want to give the number but a rule: "columns of at least 200px, as many as fit".
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));minmax(200px, 1fr): the column is at least 200px and at most one share.auto-fillinstead of a number: the browser puts as many columns of the smallest width as fit, together with the gaps.- Then the rest is shared equally (
1fr), and the columns grow to the end of the row.
See it
See how they are counted
One and the same rule in a catalogue of 960, 680 and 328px.
.catalog { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px;}Step by step, in words
- The catalogue is a grid. The columns are not given as a number but as a rule: as many as fit, each at least 200px.
- A catalogue 960px wide. The browser puts columns of 200px with a 16px gap while they fit: 4 × 200 + 3 × 16 = 848px. A fifth would reach 1064px: it does not fit.
- 960 − 848 = 112px are left. 1fr shares them equally: every column is (960 − 3 × 16) : 4 = 228px.
- At 680px three columns fit (3 × 200 + 2 × 16 = 632). A fourth would need 848. Each becomes (680 − 32) : 3 = 216px.
- At 328px, a phone, one fits: two would need 416px. It takes the whole width: 328px.
- One rule, three layouts. The number of columns follows the width of the catalogue, with no media queries. The products fill the columns row by row.
The same catalogue as a page, first on a phone (360px), then on a computer (1000px). The code is the same.
Console
Console
The columns depend on the width of the catalogue, not of the screen. A catalogue in a narrow column of the page has fewer columns even on a big screen.
Try it
One rule for both places
The page has a catalogue and a narrow column "Also bought" (220px). Both are .catalog with flex-wrap, and the last product in the catalogue is too wide. Change the rule for .catalog: grid, columns of at least 200px, as many as fit, stretched to the end. The check expects 3 columns in the catalogue, each 208px, and 1 column of 220px on the right.
Console
Solution
index.html<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Catalogue</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="page">
<main>
<h1>Catalogue</h1>
<div class="catalog">
<article class="card">
<img src="img/bread.svg" alt="" width="48" height="48">
<h2>White bread</h2>
<p>€1.20</p>
</article>
<article class="card">
<img src="img/bread.svg" alt="" width="48" height="48">
<h2>Wholegrain bread</h2>
<p>€1.90</p>
</article>
<article class="card">
<img src="img/milk.svg" alt="" width="48" height="48">
<h2>Fresh milk</h2>
<p>€1.60</p>
</article>
<article class="card">
<img src="img/milk.svg" alt="" width="48" height="48">
<h2>Yoghurt</h2>
<p>€1.10</p>
</article>
<article class="card">
<img src="img/cheese.svg" alt="" width="48" height="48">
<h2>White cheese</h2>
<p>€7.90</p>
</article>
</div>
</main>
<aside>
<h2>Also bought</h2>
<div class="catalog">
<article class="card">
<img src="img/product.svg" alt="" width="48" height="48">
<h2>Butter</h2>
<p>€3.20</p>
</article>
<article class="card">
<img src="img/tomatoes.svg" alt="" width="48" height="48">
<h2>Tomatoes</h2>
<p>€3.60</p>
</article>
</div>
</aside>
</div>
</body>
</html>
style.css* { box-sizing: border-box; }
body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; }
h1 { margin: 0 0 12px; font-size: 1.4rem; }
.page {
max-width: 900px;
display: grid;
grid-template-columns: 1fr 220px;
gap: 24px;
}
aside h2 { margin: 0 0 12px; font-size: 1.1rem; }
.catalog {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.card {
flex: 1 1 200px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
}
.card h2 { margin: 8px 0 4px; font-size: 1rem; }
.card p { margin: 0; }
display: grid and grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)). The catalogue is 900 − 220 − 24 = 656px: three columns fit (632px), and the rest makes them 208px each. In the 220px column one fits, and it becomes 220px. repeat(3, 1fr) makes three columns in the narrow column as well, where the cards are squeezed. With repeat(auto-fill, 200px) the columns do not grow, and 20px stay empty on the right.
The idea
Areas of the page
Grid also lays out the page itself: the header, the filters on the left, the catalogue, the footer. The areas can be drawn with names:
.page { display: grid; grid-template-columns: 220px 1fr; grid-template-areas: "header header" "filters main" "footer footer"; gap: 16px;}header { grid-area: header; }.filters { grid-area: filters; }- Every line in quotes is a row of the grid, and every word is a cell. One word in two cells next to each other makes an area that takes both: the header is over two columns.
grid-area: headerputs the element into the area with that name.- Every row has to have as many cells as there are columns, and an area has to be a rectangle.
Without names, an item is placed by the numbers of the lines between the columns: grid-column: 1 / 3 is from the first to the third line (two columns), and grid-column: 1 / -1 goes to the last line. grid-column: span 2 takes two columns wherever the browser puts it.
Try it
The page
Lay out the page (900px) with grid-template-areas: the header at the top over the whole width, under it the filters on the left (220px) and the catalogue on the right, the footer at the bottom over the whole width. The gap is 16px.
Console
Solution
index.html<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Catalogue</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="page">
<header>
<a href="/">The shop</a>
</header>
<aside class="filters">
<h2>Filters</h2>
<label><input type="checkbox"> In stock only</label>
<label><input type="checkbox"> Up to €5</label>
</aside>
<main>
<h1>Catalogue</h1>
<p>White bread, Wholegrain bread, Fresh milk, Yoghurt, White cheese, Apples</p>
</main>
<footer>Delivery in 2 working days</footer>
</div>
</body>
</html>
style.css* { box-sizing: border-box; }
body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; }
h1 { margin: 0 0 12px; font-size: 1.4rem; }
.page {
max-width: 900px;
display: grid;
grid-template-columns: 220px 1fr;
grid-template-areas:
"header header"
"filters main"
"footer footer";
gap: 16px;
}
header { grid-area: header; }
.filters { grid-area: filters; }
main { grid-area: main; }
footer { grid-area: footer; }
header, footer { padding: 12px 16px; background: #f3efe6; }
.filters { padding: 12px; border: 1px solid #ddd; border-radius: 8px; }
.filters h2 { margin: 0 0 8px; font-size: 1.1rem; }
.filters label { display: block; }
Two columns, 220px 1fr, and three rows of areas: "header header", "filters main", "footer footer". Each of the four elements gets a grid-area with its name. The catalogue becomes 900 − 220 − 16 = 664px. An element without grid-area is in no area: the browser puts it by itself into the first free cell. The header without grid-area goes to the top left and is 220px wide.
Try it
Find the bug
The page fell apart: the header, the filters, the catalogue and the footer are on top of each other. The console has no error. Which line is the cause?
Pick the line with the bug.
Line 7 has one cell, and there are two columns. Such a value is invalid, and the browser skips the whole grid-template-areas, not just this line. Then there are no areas named header, filters, main and footer, and the four elements with grid-area go to one and the same place outside the columns, on top of each other. It should be "footer footer". In the developer tools (F12) the property is struck through.
More precisely, if you are curious
Implicit rows. grid-template-columns and grid-template-rows describe the explicit grid. If there are more items, the browser adds rows by itself. They are implicit and are auto, as tall as the content. Their height is set with grid-auto-rows.
auto-fill and auto-fit. The difference shows when there are fewer items than columns. In a 960px catalogue with two products auto-fill keeps the four columns, and the products are 228px each. auto-fit drops the empty columns, and the two products become 472px each.
fr and the content. 1fr is minmax(auto, 1fr): the column does not get narrower than the longest word in it. If one long word stretches the column, minmax(0, 1fr) stops it.
Grid or flex. Flexbox is for one axis: a header, a row of buttons, a line in the cart. Grid is for two: a catalogue, a page, a dashboard. They often go together: the page is a grid, and the header in it is flex.
More. grid-auto-flow: dense fills empty cells with later items, but it mixes up the order on the screen compared to the tree. subgrid lets a nested grid use the columns of its parent.
Exercise
The product of the week
Make the catalogue (900px) a grid: columns of at least 200px, as many as fit, gap 16px. The product of the week is first and takes two columns. All rows are 160px tall, including the ones the browser adds by itself. The check expects 4 columns of 213px, the product of the week 442px wide and the last product 160px tall.
Console
Solution
index.html<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Catalogue</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Catalogue</h1>
<div class="catalog">
<article class="card featured">
<p class="badge">Product of the week</p>
<h2>Kashkaval cheese</h2>
<p>€9.50</p>
</article>
<article class="card">
<img src="img/bread.svg" alt="" width="48" height="48">
<h2>White bread</h2>
<p>€1.20</p>
</article>
<article class="card">
<img src="img/bread.svg" alt="" width="48" height="48">
<h2>Wholegrain bread</h2>
<p>€1.90</p>
</article>
<article class="card">
<img src="img/milk.svg" alt="" width="48" height="48">
<h2>Fresh milk</h2>
<p>€1.60</p>
</article>
<article class="card">
<img src="img/milk.svg" alt="" width="48" height="48">
<h2>Yoghurt</h2>
<p>€1.10</p>
</article>
<article class="card">
<img src="img/cheese.svg" alt="" width="48" height="48">
<h2>White cheese</h2>
<p>€7.90</p>
</article>
<article class="card">
<img src="img/apples.svg" alt="" width="48" height="48">
<h2>Apples</h2>
<p>€2.10</p>
</article>
</div>
</body>
</html>
style.css* { box-sizing: border-box; }
body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; }
h1 { margin: 0 0 12px; font-size: 1.4rem; }
.catalog {
max-width: 900px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-rows: 160px;
gap: 16px;
}
.card {
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
}
.card h2 { margin: 8px 0 4px; font-size: 1rem; }
.card p { margin: 0; }
.featured {
grid-column: span 2;
background: #fff4e0;
}
.badge { color: #c2531a; font-weight: 700; }
.catalog { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-auto-rows: 160px; gap: 16px;}.featured { grid-column: span 2; }Four columns of (900 − 48) : 4 = 213px. The product of the week is 2 × 213 + 16 = 442px, because it also takes the gap between its two columns. It and six products take eight cells: two rows. grid-template-rows: 160px gives 160px only to the first row, and the second is implicit and as tall as its content.
Test
Test yourself
Related lessons
Section titled "Related lessons"- Builds on: The HTML document · Selectors and the cascade · The box model · Flexbox: row and column
- Continues in: Responsive design · The shop front
- Tasks on this topic: CSS: how the page looks