The classic page layout — a header across the top, a sidebar down the left, content beside it, a footer at the bottom — is the problem grid was designed for. It is four regions, two of them spanning, and a page that fills the screen without the footer floating in the middle of a short page.
What does the shell look like?
<body>
<div class="shell">
<header class="site-header">...</header>
<aside class="sidebar">...</aside>
<main class="content">...</main>
<footer class="site-footer">...</footer>
</div>
</body>
.shell {
display: grid;
grid-template-columns: 16rem minmax(0, 1fr);
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
min-height: 100vh;
}
Four declarations do the structural work. The two columns are a fixed sidebar and a flexible content area. The three rows are a header that fits its content, a body that takes the rest, and a footer that fits its content. And the areas say which region goes where.
The minmax(0, 1fr) in the column definition is deliberate. A plain 1fr has an automatic minimum size, so a long word in the content would push the column wider and the layout would overflow. The zero minimum lets it shrink.
How do the regions get placed?
.site-header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.content { grid-area: main; }
.site-footer { grid-area: footer; }
Each region names its area, and the template decides where the areas sit. The header spans both columns because its area is two cells wide, and so does the footer.
The body row is 1fr, which takes all the remaining height. That single decision is what puts the footer at the bottom of the viewport on a short page, without any absolute positioning or JavaScript.
How do you make the sidebar scroll independently?
Give the sidebar its own scrolling region. It needs a bounded height for the overflow to have anything to work with, and the grid row it sits in has to be allowed to shrink.
.sidebar {
grid-area: sidebar;
overflow-y: auto;
min-height: 0; /* lets the item shrink inside its grid area */
padding: 1.5rem;
border-right: 1px solid #e2e2e2;
}
The min-height: 0 is the detail that makes it work. A grid item's default minimum height is its content height, so without it the sidebar refuses to shrink and the page grows instead of the sidebar scrolling.
How does the content area scroll?
Usually it does not scroll on its own — the page scrolls, and the header and sidebar stay put. Two approaches cover most sites.
/* approach one: the whole page scrolls, the sidebar is sticky */
.sidebar {
position: sticky;
top: 0;
align-self: start; /* stick to the top of its grid area */
max-height: 100vh;
overflow-y: auto;
}
The align-self: start matters: by default a grid item stretches to fill its area, and a stretched item has no room to stick. Releasing it lets the sticky positioning work.
/* approach two: the shell fills the screen and the main area scrolls */
.shell {
height: 100vh;
overflow: hidden;
}
.content {
overflow-y: auto;
min-height: 0;
}
The second approach gives an app-like layout where the chrome never moves, but it has a real cost: the page no longer scrolls as a document, which breaks the browser's find-in-page, the back button, and any expectation that the address bar scrolls away. For a documentation site or a blog, approach one is friendlier. For a dashboard, approach two is standard.
How does the shell collapse on a phone?
@media (max-width: 48rem) {
.shell {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"sidebar"
"main"
"footer";
}
.sidebar {
position: static;
max-height: none;
border-right: 0;
border-bottom: 1px solid #e2e2e2;
}
}
Because the layout is described by named areas, the narrow version is a different template and nothing else has to change. The elements keep their names, so no rule about their placement has to be rewritten.
How do you keep the text column readable?
A content area that stretches across a wide monitor gives you lines of 200 characters, which nobody can read. The fix is to constrain the text itself rather than the page, using a maximum width in characters and centring it inside the grid area.
.content {
grid-area: main;
padding: 2rem 1.5rem;
}
.prose {
max-width: 68ch; /* roughly 70 characters per line */
margin-inline: auto; /* centred within the content area */
}
Using ch units means the measure stays sensible whatever font the reader has chosen, and margin-inline is the logical version of the left and right margins, so it flips correctly in a right-to-left language.
How do you keep a header visible while scrolling?
Sticky positioning works inside a grid area, but the item must not be stretched to fill it, for the same reason as the sidebar.
.site-header {
grid-area: header;
position: sticky;
top: 0;
align-self: start;
z-index: 10;
background: #fff;
}
Two details matter. The align-self: start releases the item from the grid's default stretch, so it has a height to stick within. And the background colour has to be opaque, because the content scrolls under the header and a transparent one lets the text show through.
Are there other things worth handling?
Two. A print stylesheet, because printing a page with a sticky header and a dark background usually produces something unusable.
@media print {
.site-header,
.sidebar,
.site-footer { display: none; }
.shell {
display: block;
}
.prose { max-width: none; }
}
And the reader's motion preference, which is worth honouring on any page with an entrance animation.
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
That rule is a blunt instrument, and it is better to put the media query next to the animation and disable it there. But when the animation is scattered across many components, the blanket rule is a reasonable safety net.
Quick reference
- Two columns, three rows and a named-area template describe the whole shell.
minmax(0, 1fr)stops a long word from pushing the content column wide.- A
1frbody row puts the footer at the bottom of a short page. min-height: 0is what lets a grid item scroll instead of growing.- For a sticky sidebar, set
align-self: startso it is not stretched. - Redraw the area template in a media query; the markup stays the same.
Try it yourself: build the shell and make the page short, so the footer sits at the bottom of the screen. Then make the content very long and confirm the footer follows the content rather than staying pinned. Finally, switch to the height: 100vh variant and notice how the page scroll behaviour changes.