A grid that defines only columns still has to place items in rows, and the rules for how those rows are sized and spaced are where most grid surprises come from. The key idea is that a grid creates rows as needed, and those extra rows are called implicit tracks.
What is the difference between explicit and implicit tracks?
Explicit tracks are the ones you declare. Implicit tracks are created automatically when an item is placed in a row or column that does not exist yet.
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
/* no rows declared at all */
}
.grid > :nth-child(7) { grid-column: 1; } /* forces a fourth row */
With three columns declared and seven items, the grid creates a fourth row automatically. That row is an implicit track, and by default its size is determined by the content, which is usually what you want — but you cannot set its size with grid-template-rows, because that only describes the explicit rows.
To control the implicit rows, you set the size of the automatic tracks with the grid-auto-rows and grid-auto-columns properties.
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 12rem; /* every auto-created row is 12rem */
gap: 1rem;
}
How do you declare rows?
The same way as columns, and the two properties are independent. A grid can have three columns and two explicit rows, with everything beyond that being implicit.
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto 1fr; /* the first row fits its content, the second takes the rest */
height: 100%;
}
Mixing auto and 1fr is a very common pattern: the header row sizes to its content and the body row takes everything left. It is the same idea as the flexbox main and cross axes, applied to both dimensions at once.
Line names make a grid much easier to read, because you can refer to the lines by name instead of counting.
.grid {
display: grid;
grid-template-columns:
[full-start] minmax(1rem, 1fr)
[content-start] minmax(0, 60rem) [content-end]
minmax(1rem, 1fr) [full-end];
}
.header {
grid-column: full-start / full-end;
}
.main {
grid-column: content-start / content-end;
}
How does the gap work?
One property covers both axes, and the shorthand takes a row gap and a column gap. The row gap is the vertical space between rows, the column gap is the horizontal space between columns.
.grid {
display: grid;
gap: 1rem 2rem; /* 1rem between rows, 2rem between columns */
}
Using gap instead of margins is the better habit, for two reasons. It only applies between items, so there is no stray space at the edges of the container. And it does not collapse, so a row of items and a column of items behave consistently.
There is a second gap property for the space between the grid tracks and the edges of the container, which is useful for a full-bleed layout with an inner content area.
.shell {
display: grid;
grid-template-columns: 1fr;
padding: 1rem; /* space between the tracks and the container edge */
gap: 1rem;
}
How do you align the items inside their cells?
Two properties, and they work like the flexbox ones but apply to both axes at once because a grid cell is two-dimensional.
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: start; /* each item sits at the top of its cell */
justify-items: start; /* each item sits at the left of its cell */
place-items: center; /* shorthand for both */
}
The default for both is stretch, which makes the item fill the cell. That is why grid items are usually full width by default, and why a card in a grid stretches to the height of the tallest card in its row. Setting align-items: start makes each card only as tall as its own content.
The place-items shorthand is worth remembering because it does in one declaration what flexbox needs two for, and it is the quickest way to centre a single element in a grid cell.
How does the browser decide where to put items?
Items that have no explicit position are placed one after another, filling each row before moving to the next, and the placement is affected by anything already placed explicitly. Understanding the algorithm explains the "why did my item jump to the second row" questions.
default flow (row)
1 2 3 4
5 6 7 8
with an item spanning two columns in the first row
1 2 3 [wide]
4 5 6 7
The dense packing algorithm, which you enable with grid-auto-flow: dense, changes the second case: the browser backfills the hole left by the spanning item, so the later items move up into the gap. The result is tidier, and the visual order no longer matches the DOM order — so it is a trade-off rather than a free improvement.
Can a grid be a grid item?
Yes, and nesting is where grid earns its keep. A grid item can itself be a grid or a flex container, which is how a card with a grid of tags inside it, or a page shell with a grid of cards inside the main area, is built.
.shell {
display: grid;
grid-template-columns: 1fr 3fr;
}
.main {
display: grid; /* a grid inside a grid item */
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: 1rem;
}
Each grid is independent, so the inner grid does not have to know anything about the outer one, and the inner one can be responsive without any of the outer layout changing. That composability is the reason grid replaced most of the layout hacks people used to build two-dimensional arrangements by hand.
Quick reference
- Explicit tracks are the ones you declare; implicit tracks are created as items need them.
grid-auto-rowsandgrid-auto-columnssize the implicit ones.- Mix
autoand1frfor a content-sized header and a flexible body. - Line names like
content-startmake placement readable. gapbeats margins: no edge space, no collapsing.place-items: centercentres an item in its cell on both axes.
Try it yourself: build a grid with three columns and six items, then add a seventh and watch an implicit row appear. Set grid-auto-rows to a fixed size and confirm it applies. Finally, add align-items: start and watch the cards stop stretching to the height of the tallest one.