A grid is more than equal cells. Items can span several columns or rows, and the whole layout can be described with named areas, which turns the CSS into a picture of the page. Both features are what make grid the right tool for a full page shell.
How does an item span cells?
By naming the grid lines it starts and ends on. The grid-column property takes a start and an end, and the shorthand for it is span for a fixed number of tracks.
.grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 1rem;
}
.wide {
grid-column: span 3; /* three columns wide */
}
.full {
grid-column: 1 / -1; /* every column, whatever the count */
}
Rows work the same way with grid-row. And an item can span in both directions at once, which is how a large panel next to a stack of small ones is built.
.panel {
grid-column: span 4;
grid-row: span 2;
}
What happens to the other items?
Items with an explicit position are placed first, and the rest are placed around them in the free space. That is the behaviour to keep in mind: a single spanning item changes where everything else lands, and the result can be surprising if you were expecting a simple row flow.
/* the dense packing algorithm fills the gaps left by earlier items */
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-flow: dense;
}
grid-auto-flow: dense tells the browser to backfill the holes that a spanning item leaves, rather than moving on and leaving a gap. It produces a tidier result when items have mixed sizes, at the cost of the visual order no longer matching the DOM order — so, like everything else that reorders things, use it carefully.
What are grid template areas?
A named area is a label attached to a rectangle of the grid, and the template property draws the whole layout as a set of labels. It is the most readable way to describe a page shell that has ever been written in CSS.
.shell {
display: grid;
grid-template-columns: 16rem 1fr;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }
The template is a grid of strings, one per row, and each string is a row of names. Every cell in the rectangle must be filled, and the names must form a rectangle — if a name appears in an L-shape, the whole declaration is invalid and the grid falls back to no areas at all.
"header header"
"sidebar main"
"footer footer"
each label must form a rectangle:
"header header"
"sidebar main" <- header does not touch the row below, which is fine
"footer footer" because header is only in the first row
Because the areas are named, the markup does not have to match the visual order. You can rearrange the whole page by editing the template, and each element keeps the same grid-area name.
How do you leave a cell empty?
With a full stop. A cell containing a single dot is a cell that nothing occupies, and it lets you leave a deliberate hole without using an empty div.
.shell {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-areas:
"banner banner banner"
". promo promo";
}
Can the areas change at different sizes?
Yes, and this is where the named-area approach really pays off. A media query can redraw the template, and because the elements are placed by name, the CSS is all that has to change.
.shell {
display: grid;
grid-template-areas:
"header"
"sidebar"
"main"
"footer";
grid-template-columns: 1fr;
}
@media (min-width: 48rem) {
.shell {
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
grid-template-columns: 16rem 1fr;
}
}
The markup stays exactly the same in both layouts. That is the advantage over positioning items with line numbers, where a different layout usually means different line numbers and therefore a different set of rules.
How do the line numbers work?
Grid lines are numbered from 1 at the start edge, and the numbers count the gaps rather than the cells, which is the detail that trips people up. A grid with three columns has four lines: 1, 2, 3 and 4.
columns: [ a ][ b ][ c ]
lines: 1 2 3 4
grid-column: 1 / 3 -> starts at line 1, ends at line 3 = columns a and b
grid-column: 2 / 4 -> starts at line 2, ends at line 4 = columns b and c
That is why the end value is exclusive. An item placed from line 1 to line 3 covers two columns, not three, because the end line is where the item stops rather than a line the item covers.
Negative numbers count from the end, which is what makes 1 / -1 work: -1 is the last line, so the item spans the full width without you having to know how many columns there are.
Why name the lines?
Because named lines are self-documenting and they survive a change to the column count. If you name the lines, you can place items by name and never count again.
.grid {
display: grid;
grid-template-columns:
[sidebar-start] 16rem
[sidebar-end content-start] 1fr
[content-end];
}
.sidebar { grid-column: sidebar-start / sidebar-end; }
.content { grid-column: content-start / content-end; }
The square brackets in the template can hold several names, which is what lets a line be called both "sidebar-end" and "content-start".
When should you avoid dense packing?
Whenever the order matters. Dense packing reorders items visually, so a list of articles, a set of form fields, or a navigation menu will no longer be read in the order it appears in the markup. A screen reader user tabbing through a form whose fields were moved would be moved around with them, and the visual and the logical order would disagree.
For a gallery of tiles where order does not matter, dense packing is a genuine improvement. For anything a person reads in sequence, leave it off.
Quick reference
grid-column: span 3spans three tracks;1 / -1spans the whole grid.- Items with explicit positions are placed first, and the rest fill around them.
grid-auto-flow: densebackfills the holes, at the cost of the visual order.grid-template-areasdescribes the layout as a picture, and every area must be a rectangle.- A dot leaves a cell empty.
- Named areas let a media query redraw the layout without touching the markup.
Try it yourself: build the header, sidebar, main and footer shell with named areas, then redraw the template in a media query for a narrow screen. Next, remove a name from one cell so the rectangle breaks, and see the whole area layout disappear — that is what an invalid template does.