Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Intermediate+200 XP

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.

40 min August 2026
// Part 01 — Planning a Layout Before Writing Any CSS

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.

The habit worth building before touching any Grid property
/*
  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.

💡 Note
Every layout in this module uses real, complete HTML alongside the CSS — not fragments. Copy either example directly into a blank HTML file with a linked stylesheet and it will render exactly as described.
// Part 02 — The Holy Grail Layout

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.

The complete HTML
<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>
The complete CSS — the entire layout
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.

🎯 Pro Tip
Notice that 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.

Collapsing to a single column below 768px
@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.

// Part 03 — A Responsive Image Gallery

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.

The complete HTML
<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>
The complete CSS
.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.

auto-fill — empty tracks are preserved
.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. */
auto-fit — empty tracks collapse to zero
.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. */
⚠️ Important
For a photo gallery or card grid where items should visually fill the row even when there aren't quite enough of them, 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.
// Part 04 — A Real Dashboard Layout

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).

The dashboard HTML
<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>
The page shell — named areas, exactly like the holy grail layout
.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; }
The widget area — its own nested grid, with irregular spans
.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.

A peek at what's coming — Flexbox INSIDE a Grid-placed 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;
}
// Part 05 — Debugging a Real Layout Visually

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.

Turning it on in Chrome DevTools
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 label

Firefox'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.

🎯 Pro Tip
When a layout looks subtly wrong — an item in the wrong cell, an implicit row a different height than expected — turning on the Grid overlay first, before touching the CSS, answers the question "is this a track-sizing problem or a placement problem" in seconds, rather than guessing from reading the stylesheet alone.
// Part 06 — Real World
💼 What This Looks Like at Work

A Real Estate Listings Gallery at an Austin Proptech Startup

Scenario — Real estate proptech startup, Austin · Listings page redesign

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.

The old listings grid
.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.

The rebuilt listings grid
.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.

// Part 07 — Misconceptions

Four Misconceptions About Real-World Grid Layouts

✕ ""A holy grail layout needs min-height: 100vh on the FOOTER to stay at the bottom""
The min-height belongs on the GRID CONTAINER (the page shell), not the footer. It is the container filling the full viewport height, with its middle row set to 1fr, that pushes the footer down — the footer itself just needs its row to be auto-sized to its own content.
✕ ""auto-fill and auto-fit are basically the same thing and it doesn't matter which you pick""
They behave identically when there are enough items to fill every possible column — the difference only appears with fewer items than would fill a row. auto-fill leaves the extra tracks empty but reserved; auto-fit collapses them to zero and lets the real items stretch to fill the space. Picking the wrong one produces a gallery that looks oddly left-aligned instead of filling its row.
✕ ""A dashboard-style layout with irregularly sized cards needs a completely different technique than a simple page shell""
It is the same grid-template-areas and line-based placement techniques from the previous module, just nested — a page-level shell built from named areas, containing a widget area that is itself a separate grid using span for irregular card sizes.
✕ ""Once you're using Grid for a layout, every part of it — down to a single card's internals — should also be Grid""
Using Grid for page-level structure and Flexbox for the internal layout of an individual component (like a card with a label, a value, and a trend indicator) inside a grid cell is extremely common and often the more natural fit — this exact combination is the entire subject of the next module.
// Part 08 — Interview Prep

5 Interview Questions — With Complete Answers

Walk through how you would build a classic header/sidebar/content/footer layout with CSS Grid.
Set display: grid on the page container, define grid-template-columns for a fixed sidebar width plus a flexible 1fr content column, define grid-template-rows as auto 1fr auto so the header and footer size to their content while the middle row absorbs remaining space, and draw the layout with grid-template-areas. Set min-height: 100vh on the container so the footer stays pinned to the bottom of the viewport even on short pages, and assign each element its grid-area.
Why is grid-template-areas particularly well suited to making a layout responsive?
Reshaping a layout at a breakpoint — for example, collapsing a sidebar to a full-width row above the content on mobile — only requires redeclaring grid-template-columns and grid-template-areas on the container inside a media query. The HTML and each element's individual grid-area assignment never need to change, since the area names stay the same; only where the container draws them does.
How would you build a photo gallery that automatically adjusts its column count to the viewport width, without media queries?
grid-template-columns: repeat(auto-fit, minmax(minWidth, 1fr)) on the gallery container. It fits as many columns of at least minWidth as will comfortably fit the current container width, and auto-fit collapses any leftover empty tracks so the real items stretch to fill the row — no explicit breakpoints are needed because the column count is a natural consequence of available width divided by the minimum card size.
How would you build a dashboard where one widget needs to be visually larger than the others in the same grid?
Define the widget grid with a fixed column count and grid-auto-rows for consistent row height, then give the larger widget grid-column: span 2 and/or grid-row: span 2 to make it occupy a 2x1, 1x2, or 2x2 area among the otherwise uniform 1x1 cells — this is the line-based placement span keyword applied to build an intentionally irregular grid.
Is it correct to use Grid for a component's internal layout as well as the page structure around it, or should you pick one system for the whole page?
It is correct, and extremely common, to mix them: Grid for page-level and section-level structure (since that is genuinely two-dimensional — rows and columns of distinct regions), and Flexbox for the internal layout of individual components placed inside those grid cells, where content is typically laid out along a single axis. Nesting a Flexbox container inside a Grid item is completely normal.
// Common Mistakes

Real-Layout Grid Mistakes Beginners Make Constantly

Before
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 */
After
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;
}
Without min-height: 100vh on the grid container, a short page leaves the footer sitting immediately below the content instead of pinned to the bottom of the viewport — the classic "sticky footer" behaviour requires the container to actually fill the screen height.
Before
.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 */
After
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
A hardcoded column count only looks correct at the specific viewport width it was designed against. auto-fit with minmax() makes the column count a natural consequence of available space, correct at every width without a single media query.
Before
.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 */
After
.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 */
}
span 2 on grid-row only means something visually consistent if the rows it is spanning actually have a defined height — without grid-auto-rows, implicit rows size to content, and a "2-row" span can end up barely taller than a 1-row widget.
// Error Library

Errors and Rendering Bugs You Will Hit Building Real Layouts — And Exactly Why

A "sticky" footer sits in the middle of a short page instead of at the bottom
Cause: The grid container has no min-height: 100vh (or equivalent), so the container — and therefore the grid — is only as tall as its actual content, leaving no leftover space for the 1fr middle row to absorb and push the footer down.
Fix: Add min-height: 100vh to the grid container (usually body or a top-level wrapper), and remove any default margin on body that could add unwanted scroll space.
An image gallery leaves a large, uneven empty gap on the right side of the last row
Cause: grid-template-columns uses auto-fill instead of auto-fit with too few items to fill a complete row — auto-fill reserves the extra empty tracks rather than collapsing them, so the real items do not stretch to fill the available width.
Fix: Switch to repeat(auto-fit, minmax(min, 1fr)) if the intent is for existing items to fill the row when there aren't enough to complete it — this is the correct default for most photo/card galleries.
A dashboard widget with grid-column: span 2 renders at half the expected width
Cause: The number of explicit columns defined on the container is smaller than assumed — span 2 in a 2-column grid takes up the ENTIRE row, not half of it, which can look identical to a bug depending on the design.
Fix: Confirm the actual column count set in grid-template-columns on the container matches what the span values assume — use the browser's Grid inspector (Part 05) to see the real track boundaries directly on the page.
object-fit: cover appears to have no effect on a gallery image
Cause: The img element has no explicit width and height (or an aspect-ratio) set on it — object-fit only controls HOW an image fills its box; it does nothing if the box itself is still sized to the image's natural dimensions.
Fix: Set width: 100%; height: 100% on the image (with the sizing coming from a parent that has a fixed size or aspect-ratio), so there is an actual box for object-fit: cover to fill and crop within.
A nested grid (a widget area inside a page shell) doesn't line up with the outer grid's columns
Cause: Two separate display: grid containers, nested inside one another, define their own independent tracks by default — there is no automatic alignment between an outer grid's columns and an inner grid's columns unless explicitly designed to match.
Fix: Either explicitly match the track definitions between the two grids, or use CSS subgrid (grid-template-columns: subgrid on the inner grid) so it inherits the outer grid's track sizing directly — subgrid has strong modern browser support but is worth checking against your project's specific browser support requirements before relying on it.

🎯 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 Each
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...