CSS Grid in Practice — Real Layouts
Real page layouts built with Grid — holy grail layouts, image galleries, and dashboards that would be painful with Flexbox alone.
Start From the Wireframe, Not the Properties
The previous module covered every individual Grid property in isolation. Real layouts are never built by reaching for properties one at a time — they start with a rough wireframe of named regions, and the CSS follows directly from that drawing. This module builds three real, complete layouts end to end: a classic holy grail page shell, a responsive image gallery, and a dashboard combining several Grid techniques at once. Each one starts the same way — sketch the regions first, then let grid-template-areas (from the previous module's Part 04) turn that sketch directly into CSS.
/*
header header
sidebar main
sidebar footer
*/
/* Write the wireframe as a comment FIRST. The grid-template-areas
declaration you end up writing will look almost identical to it. */This matters more than it might sound like it should. Engineers who reach straight for grid-column/grid-row line numbers on a full-page layout tend to produce CSS that works but is genuinely hard for the next person to read — a page shell built from named areas reads like documentation of itself, months later, without needing comments at all.
Header, Sidebar, Main Content, Footer — In About 12 Lines
The "holy grail" layout is a long-standing name in CSS for a specific, extremely common page shape: a full-width header, a full-width footer, and a middle row split into a fixed-width sidebar and a flexible main content area. It earned the name because, before Grid existed, it was genuinely difficult to build correctly with floats or early Flexbox — getting the sidebar and main content to reliably match heights while the footer stayed pinned below both required real workarounds. With Grid, it stops being a "holy grail" and becomes a small, boring amount of CSS.
<body>
<header class="site-header">Site Header</header>
<nav class="sidebar">Sidebar Nav</nav>
<main class="content">Main Content</main>
<footer class="site-footer">Footer</footer>
</body>body {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar content"
"footer footer";
min-height: 100vh;
margin: 0;
}
.site-header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.content { grid-area: content; }
.site-footer { grid-area: footer; }Three details worth calling out explicitly. First, min-height: 100vh on the grid container is what pins the footer to the bottom of the viewport even when the content is short — without it, the grid would only be as tall as its content, and a short page would show the footer sitting immediately below a nearly-empty content area rather than at the true bottom of the screen. Second, the header and footer rows both use auto, meaning they size to their own content's height rather than a hardcoded pixel value — genuinely important if either region's content (a banner, a multi-line footer) can vary. Third, the middle row is 1fr, which is what makes it absorb all remaining vertical space and push the footer down, exactly like the fr behaviour from the previous module's Part 02, just applied to the row axis instead of columns.
sidebar and content automatically match heights — this was one of the genuinely hard parts of the pre-Grid version of this layout. Because both regions occupy the same grid row, they share that row's height by definition; there is no separate "equal height columns" technique needed, unlike with floats.Making the holy grail responsive
On narrow screens, a fixed 240px sidebar next to main content is rarely the right layout — the standard mobile pattern collapses the sidebar to a full-width row above the content instead. Because the entire layout lives in one grid-template-areas declaration, the responsive version is just a second declaration of the same property inside a media query — no restructuring of the HTML, and no separate mobile-specific markup.
@media (max-width: 768px) {
body {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"sidebar"
"content"
"footer";
}
}
/* Same HTML, same grid-area assignments on each element — only the
CONTAINER's track definitions and area drawing changed. */This is the single biggest practical advantage grid-template-areas has for responsive work specifically: reordering, or entirely reshaping, a layout at a breakpoint rarely requires touching the HTML or the individual items' rules at all — only the container's area drawing changes. Media queries are covered in full depth in the module right after the next one in this track.
auto-fill / auto-fit + minmax() — A Gallery That Wraps Itself
The previous module introduced repeat(auto-fill, minmax(min, 1fr)) as the pattern for a self-wrapping grid. Here it gets built out into a genuinely complete, real image gallery — square thumbnails, consistent gaps, and correct wrapping behaviour at any viewport width, without a single media query.
<div class="gallery">
<figure class="gallery-item"><img src="/photos/01.jpg" alt="Sunset over the harbor"></figure>
<figure class="gallery-item"><img src="/photos/02.jpg" alt="Downtown skyline at dusk"></figure>
<figure class="gallery-item"><img src="/photos/03.jpg" alt="Mountain trail in autumn"></figure>
<!-- ...as many more <figure> items as the gallery actually has -->
</div>.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
padding: 16px;
}
.gallery-item {
margin: 0;
aspect-ratio: 1 / 1; /* forces a perfect square, regardless of the image's real dimensions */
overflow: hidden;
border-radius: 8px;
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: cover; /* fills the square without distorting the image's aspect ratio */
display: block;
}aspect-ratio combined with object-fit: cover is what makes every thumbnail a uniform square even though the source photos have completely different original dimensions — the image is cropped to fill the square, never stretched. This pairing is extremely common in real gallery and card-grid UIs, not specific to Grid, but it is worth calling out here because it is what makes the example actually look like a gallery rather than a grid of mismatched rectangles.
auto-fill vs auto-fit — the difference that only shows up with too few items
Both keywords fit as many minmax()-sized columns as will comfortably fit the container's width, and both wrap to a new row automatically as the container narrows. They are identical in almost every practical case — the difference only becomes visible when the number of actual items is smaller than the number of columns that would fit.
.gallery {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
/* Say the container is wide enough for 5 columns, but there are only 3 items.
auto-fill still RESERVES 5 column tracks — the 2 empty ones just render
as blank space, and the 3 real items stay pinned to 1fr each, NOT
stretching to fill the row. */.gallery {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
/* Same scenario — 3 items, room for 5 columns.
auto-fit COLLAPSES the 2 empty tracks to 0 width, and the 1fr on the
remaining 3 items means they stretch to fill the freed-up space. */auto-fit is almost always the correct choice —auto-fill's reserved-but-empty tracks tend to look like a layout bug (uneven, oddly left-aligned content) rather than an intentional design. Reach for auto-fill specifically when you want items to stay a fixed size and NOT stretch, even with room to spare — a strip of fixed-size thumbnail previews is a reasonable case for that.Combining Named Areas, Nested Grids, and Irregular Spans
A dashboard is a genuinely good test of whether Grid has actually clicked, because it usually needs several techniques from the previous module working together at once: a page-level shell (named areas, exactly like Part 02), a widget area with cards of genuinely different sizes (line-based spanning), and individual widgets that are themselves grids or flex containers internally (nested layout contexts).
<body class="dashboard">
<header class="db-header">Analytics Dashboard</header>
<nav class="db-nav">Nav</nav>
<main class="db-widgets">
<section class="widget widget-large">Revenue Trend</section>
<section class="widget">Active Users</section>
<section class="widget">Conversion Rate</section>
<section class="widget widget-wide">Top Referral Sources</section>
<section class="widget">Server Uptime</section>
</main>
</body>.dashboard {
display: grid;
grid-template-columns: 220px 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"header header"
"nav widgets";
min-height: 100vh;
}
.db-header { grid-area: header; }
.db-nav { grid-area: nav; }
.db-widgets { grid-area: widgets; }.db-widgets {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 160px;
gap: 20px;
padding: 20px;
}
.widget-large {
grid-column: span 2;
grid-row: span 2; /* a 2x2 "featured" widget among the standard 1x1 ones */
}
.widget-wide {
grid-column: span 2; /* wide but not tall — spans 2 columns, standard 1 row */
}
.widget {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
}.db-widgets is both a grid item (of the outer page shell) and a grid container (of the widgets inside it) — nesting grids like this is completely normal and is exactly how real dashboards are structured, rather than trying to express the entire page as one flat grid. Each widget's span (span 2, span 2 / span 2) is set individually, which is precisely the line-based placement technique from the previous module's Part 05, applied here to build a genuinely irregular, magazine-style grid rather than a uniform table of equal cells.
What each individual widget does internally has nothing to do with Grid
A single widget's own internal layout — say, a metric card with a label at the top, a large number in the middle, and a small trend indicator pinned to the bottom-right — is a completely separate layout decision from how the widget is placed on the page. This is a genuinely important idea that the next module in this track is built entirely around: it is extremely common, and correct, to use Grid for the page-level and widget-grid structure shown here, while using Flexbox for what happens inside each individual widget.
.widget-large {
grid-column: span 2;
grid-row: span 2;
display: flex; /* the widget's OWN internal layout is Flexbox, not Grid */
flex-direction: column;
justify-content: space-between;
}The Browser DevTools Grid Inspector
Every layout in this module is genuinely difficult to get pixel-perfect on the first attempt purely by reading CSS — real Grid debugging happens visually, in the browser. Chrome, Firefox, and Safari all ship a dedicated Grid inspector that overlays line numbers, track sizes, and named areas directly on the rendered page.
1. Open DevTools (Cmd+Option+I on Mac, F12 on Windows/Linux)
2. Select the Elements panel
3. Find an element with display: grid applied
4. Click the small "grid" badge next to it in the Elements tree
— this toggles a colored overlay directly on the page showing
every line number, track boundary, and (if used) named area labelFirefox's implementation is widely considered the most complete of the three — its Grid inspector panel includes a toggle specifically for displaying grid-template-areas names directly on the overlay, and an option to extend grid lines infinitely across the full page, which makes it dramatically easier to see whether two separate grid containers happen to align.
A Real Estate Listings Gallery at an Austin Proptech Startup
A front-end engineer at an Austin proptech startup is asked to rebuild the property listings grid. The old version used display: inline-block cards with a manually calculated width: 32% and margin gaps between them, hardcoded for exactly three columns — and it silently broke on smaller laptop screens, wrapping mid-row and leaving an ugly gap where the third card should have been, because 3×32% plus two sets of margin no longer fit.
.listings {
display: block;
}
.listing-card {
display: inline-block;
width: 32%;
margin-right: 2%;
margin-bottom: 24px;
vertical-align: top;
}
.listing-card:nth-child(3n) {
margin-right: 0; /* manually zeroing margin every 3rd card */
}
/* Hardcoded for exactly 3 columns. Breaks the moment the viewport
is too narrow for 3 cards but not narrow enough to trigger a
redesign — a genuinely common, easy-to-miss dead zone. */The rebuild
The engineer replaces it with exactly the gallery pattern from Part 03 of this module — auto-fit and minmax() — with the minimum column width tuned to the actual listing card's comfortable minimum size rather than an arbitrary percentage.
.listings {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}
/* No hardcoded column count, no manual margin math, no :nth-child
exceptions. The number of columns is now a NATURAL CONSEQUENCE
of the viewport width and the card's minimum comfortable size —
not a number chosen once and forgotten. */On a wide desktop monitor it renders four columns; on a laptop, three; on a tablet, two — all without a single media query, and without the dead-zone gap the old percentage-based version produced. When the design team later asks for a slightly wider card to fit a new "verified listing" badge, the fix is changing one number — 280px to 320px — with the column count adjusting itself automatically at every screen size, instead of re-deriving a new percentage and a new :nth-child rule by hand.
Four Misconceptions About Real-World Grid Layouts
5 Interview Questions — With Complete Answers
Real-Layout Grid Mistakes Beginners Make Constantly
body {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar content"
"footer footer";
}
/* min-height never set — body only grows as tall as its content */body {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar content"
"footer footer";
min-height: 100vh;
margin: 0;
}.gallery {
display: grid;
grid-template-columns: repeat(4, 1fr); /* hardcoded column count */
}
/* Looks fine on the design mockup's screen size, breaks (either
too cramped or with awkward empty space) at every other width */.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}.widget-large {
grid-column: span 2;
grid-row: span 2;
}
/* grid-auto-rows never set on the container — implicit rows size
to their content, so the "2x2" widget doesn't actually look
twice as tall as a normal widget */.db-widgets {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 160px; /* every implicit row is a fixed, predictable height */
}
.widget-large {
grid-column: span 2;
grid-row: span 2; /* now genuinely spans 2 real 160px rows */
}Errors and Rendering Bugs You Will Hit Building Real Layouts — And Exactly Why
🎯 Key Takeaways
- ✓Sketch a layout as named regions before writing CSS — grid-template-areas turns that sketch directly into working code, and the CSS ends up reading like documentation of the wireframe.
- ✓The holy grail layout (header/sidebar/content/footer) needs min-height: 100vh on the grid container and a 1fr middle row to keep the footer pinned to the bottom of short pages.
- ✓grid-template-areas makes responsive reshaping cheap — a media query redeclaring the container's columns and area drawing can restructure the entire page without touching the HTML or any individual element's rules.
- ✓repeat(auto-fit, minmax(min, 1fr)) is the standard pattern for a self-wrapping gallery or card grid — auto-fit collapses empty tracks so existing items stretch to fill the row; auto-fill preserves them as reserved-but-empty.
- ✓A dashboard combines named-area page structure with line-based span placement for irregularly sized widgets — and it is completely normal for those widgets to nest their own grid or flex layout internally.
- ✓Grid for page/section structure and Flexbox for a component's internal layout is a standard, correct combination — not a compromise, and the exact subject of the next module.
- ✓The browser DevTools Grid inspector (Chrome, Firefox, Safari all have one) overlays real line numbers, track sizes, and area names directly on the page — it is the fastest way to debug a layout that looks subtly wrong.
What comes next
Grid and Flexbox now both fully in hand — the next module builds the actual decision framework for choosing between them, and shows the pattern this module previewed: using both together in the same page.
Next Module → Flexbox vs Grid — When to Use EachDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.