Grid Advanced
⚡ Chapter 4 — Grid Advanced
Chapter 3 covered the mechanics of CSS Grid — defining tracks, placing items, and naming areas. This chapter pushes into the techniques that make Grid feel like a superpower: auto-fit and auto-fill for truly responsive layouts without a single media query, named grid lines for surgical placement, dense packing for gallery-style grids, and stacking items inside the same cell. By the end, you will be writing professional-grade layouts that adapt intelligently to any screen size.
1 — auto-fill vs auto-fit
Both keywords go inside repeat() as a replacement for a fixed count. Instead of telling the browser how many tracks to create, you tell it to create as many as fit:
/* Instead of a fixed number… */
grid-template-columns: repeat(3, 200px);
/* …let the browser decide how many tracks fit */
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
The difference only shows up when items do not fill all available space — for example, you have 3 cards but the container is wide enough for 6.
- auto-fill — creates as many tracks as the minimum allows, including empty ones. The filled items share space with those phantom tracks.
- auto-fit — creates the same tracks, but then collapses the empty ones to zero width. The filled items expand to take up all the available space.
auto-fit is almost always what you want for card grids and galleries — you want items to fill the available width. Use auto-fill when you want the grid to reserve space for items that might appear later (e.g., a fixed-width grid that gets items added dynamically), or when you want to prevent cards from stretching too wide when there are only a few of them.
2 — The Intrinsically Responsive Grid
Combine auto-fit with minmax() and you get the most powerful layout one-liner in CSS — a grid that automatically adjusts its column count to the container width, with zero media queries:
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 24px;
}
/* Reading this declaration:
"Create as many 1fr columns as will fit,
where each column is at least 240px wide"
At 1200px: fits 4 columns (300px each)
At 900px: fits 3 columns (300px each)
At 560px: fits 2 columns (280px each)
At 250px: fits 1 column (250px)
No media queries. No breakpoints. Just math. */
repeat(auto-fit, minmax(0, 1fr)) gives you the same behaviour as repeat(auto-fit, 1fr) — columns never collapse because 0 is always satisfiable. Always use a meaningful pixel minimum that reflects the actual smallest usable size for the content inside.
max-width on the grid: .card-grid { max-width: 1200px; margin-inline: auto; }. The grid reflows automatically up to 1200px, then the container centres.
3 — Named Grid Lines
Grid lines have numbers by default (1, 2, 3… and -1, -2, -3 from the end). You can also give them names using square brackets in grid-template-columns or grid-template-rows. Named lines make placement declarations read like English instead of coordinates.
.layout {
display: grid;
/* Name the lines in brackets before/after each track size */
grid-template-columns:
[full-start]
1fr
[main-start]
minmax(0, 720px)
[main-end]
1fr
[full-end];
grid-template-rows:
[header-start] 60px
[header-end content-start] 1fr
[content-end];
/* A line can have multiple names — separate them with a space */
}
/* Place items by name instead of number */
.hero {
grid-column: full-start / full-end; /* full bleed */
grid-row: header-start / content-end;
}
.article {
grid-column: main-start / main-end; /* constrained centred column */
}
grid-template-areas, the browser automatically creates named lines for you — every area named foo generates column lines foo-start and foo-end, and the same for rows. You can use those auto-generated names in grid-column and grid-row even if you are not using grid-area on the item itself.
.page {
display: grid;
grid-template-columns: 200px 1fr;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
}
/* These named lines are created automatically:
column: header-start, header-end, sidebar-start,
sidebar-end / main-start, main-end, footer-start, footer-end
row: header-start, header-end, sidebar-start,
sidebar-end, main-start, main-end, footer-start, footer-end */
/* A decorative overlay that sits in the "main" column */
.overlay {
grid-column: main-start / main-end;
grid-row: header-start / footer-end;
}
4 — grid-template-areas in Depth
You covered the basics in Chapter 3. Let's go deeper with a real magazine-style layout that mixes multiple area sizes, uses dots for empty cells, and rewires completely on mobile.
.magazine {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
grid-template-rows: auto auto auto auto;
gap: 16px;
grid-template-areas:
"hero hero promo" /* row 1 */
"hero side1 promo" /* row 2 — hero spans 2 rows */
"feature side1 side2" /* row 3 — side1 spans 2 rows */
"feature . side2"; /* row 4 — dot = empty cell */
}
.hero { grid-area: hero; }
.promo { grid-area: promo; }
.side1 { grid-area: side1; }
.side2 { grid-area: side2; }
.feature { grid-area: feature; }
/* On tablet: 2-column with different region arrangement */
@media (max-width: 800px) {
.magazine {
grid-template-columns: 1fr 1fr;
grid-template-areas:
"hero hero"
"promo side1"
"feature side2";
}
}
/* On mobile: single column, reading order */
@media (max-width: 500px) {
.magazine {
grid-template-columns: 1fr;
grid-template-areas:
"hero"
"feature"
"promo"
"side1"
"side2";
}
}
.) creates an intentionally empty cell — useful for maintaining structural alignment when not every zone has content at every breakpoint.grid-template-areas declaration, so a layout that looks correct may be falling back silently. Check DevTools grid overlay when things look off.
5 — Dense Packing with grid-auto-flow: dense
When some items span multiple columns or rows, the auto-placement algorithm leaves gaps — cells it cannot fill because the next item is too wide. By default those gaps remain empty. Adding dense instructs the browser to go back and fill them with later (smaller) items.
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-rows: 200px;
grid-auto-flow: dense; /* back-fill any gaps */
gap: 8px;
}
/* Landscape photos span 2 columns, portraits are 1×1 */
.gallery .landscape { grid-column: span 2; }
.gallery .tall { grid-row: span 2; }
.gallery .featured { grid-column: span 2; grid-row: span 2; }
.gallery img {
width: 100%;
height: 100%;
object-fit: cover;
}
6 — Overlapping Items and z-index
Unlike Flexbox, CSS Grid lets you place multiple items into the same cell or area. They stack on top of each other. Use z-index to control which appears in front — and unlike static positioned elements, grid items respond to z-index without needing position: relative.
.card {
display: grid;
grid-template: 1fr / 1fr; /* one row, one column */
height: 240px;
border-radius: 8px;
overflow: hidden;
}
/* All three children share the same cell — they stack */
.card > * {
grid-column: 1;
grid-row: 1;
}
.card__image { z-index: 1; object-fit: cover; width: 100%; height: 100%; }
.card__overlay { z-index: 2; background: linear-gradient(to top, rgba(0,0,0,.8) 0%, transparent 60%); }
.card__text {
z-index: 3;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 20px;
}
grid-template: 1fr / 1fr is a shorthand for grid-template-rows: 1fr; grid-template-columns: 1fr; — a single row and single column, each taking all available space.7 — Advanced Responsive Patterns
Pattern 1 — Sidebar that wraps below content
.with-sidebar {
display: grid;
grid-template-columns: minmax(260px, 1fr) minmax(0, 2fr);
gap: 24px;
}
/* When the container is narrower than 520px (260+260),
the second column can no longer maintain its minimum and
the layout naturally breaks to a single column.
No media query needed. */
minmax on both columns. When the container cannot satisfy both minimums simultaneously, the grid implicitly wraps — but only works when grid-template-columns creates 2 columns in the first place. Pair with a media query if you need guaranteed wrapping at a specific breakpoint.Pattern 2 — Full-bleed sections in a constrained layout
/* Three columns: edge | content | edge
Content is capped at 720px and centred.
Some sections can "break out" to full width. */
.article-page {
display: grid;
grid-template-columns:
[full-start]
1fr
[content-start]
minmax(0, 720px)
[content-end]
1fr
[full-end];
}
/* All direct children default to the content zone */
.article-page > * {
grid-column: content; /* shorthand: content-start / content-end */
}
/* Full-bleed hero, pull-quotes, or banners break out */
.full-bleed {
grid-column: full; /* full-start / full-end */
}
content or full in a shorthand, the browser expands it to content-start / content-end automatically — this works because named lines ending in -start and -end are treated as named areas by the placement algorithm.Pattern 3 — Equal-height rows across multiple cards
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
grid-auto-rows: 1fr; /* all rows same height */
align-items: stretch;
}
/* Each card is itself a grid — interior rows align across cards */
.card {
display: grid;
grid-template-rows: auto 1fr auto; /* header | body | footer */
}
/* card__header: auto height */
/* card__body: grows (1fr) */
/* card__footer: auto (button) — always pinned to bottom */
8 — Quick Reference
auto-fill vs auto-fit
| Keyword | Empty tracks | Item width with few items | Best for |
|---|---|---|---|
auto-fill |
Kept (phantom slots) | Stays at minimum — does not stretch | Grids where empty slots should reserve space |
auto-fit |
Collapsed to 0 | Expands to fill the container | Card grids, galleries — most common use case |
grid-auto-flow values
| Value | Behaviour | Warning |
|---|---|---|
row |
Fill rows left→right, new row when full (default) | — |
column |
Fill columns top→bottom, new column when full | — |
dense |
Back-fill gaps left by spanning items | Visual ≠ DOM order — accessibility risk |
row dense |
Row direction + dense back-fill | Same accessibility warning |
Named line syntax
| Pattern | Effect |
|---|---|
[name] before a track | Names the line before that track |
[name1 name2] | One line can have multiple names |
grid-column: name-start / name-end | Place item using line names |
grid-column: name (single name) | Shorthand — expands to name-start / name-end if both exist |
Auto-names from grid-template-areas | Area "foo" creates foo-start, foo-end on both axes |
✏️ Exercises
Each exercise builds on techniques from this chapter. Attempt the solution before revealing the answer.
object-fit: cover. Every third card (nth-child) should span 2 columns (a featured photo). Use grid-auto-flow: dense so the featured cards don't leave gaps.repeat(auto-fit, minmax(180px, 1fr)). Give .card:nth-child(3n) a grid-column: span 2. Add grid-auto-flow: dense to the container..gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
grid-auto-rows: 220px;
grid-auto-flow: dense;
gap: 8px;
}
.gallery .card:nth-child(3n) {
grid-column: span 2;
}
.gallery img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.full-bleed breaks out to the full container width. Use named grid lines ([full-start], [content-start], [content-end], [full-end]) and set all direct children to the content zone by default.1fr [content-start] minmax(0, 680px) [content-end] 1fr. Set .article > * to grid-column: content-start / content-end. Override with .full-bleed { grid-column: full-start / full-end; }..article {
display: grid;
grid-template-columns:
[full-start]
1fr
[content-start]
minmax(0, 680px)
[content-end]
1fr
[full-end];
}
.article > * {
grid-column: content-start / content-end;
}
.full-bleed {
grid-column: full-start / full-end;
}
display: grid for the stacking — no absolute positioning.grid-template: 1fr / 1fr). Give all children grid-column: 1; grid-row: 1. Use z-index to layer them. Make the text container display: flex; align-items: flex-end.<div class="card">
<img class="card__image" src="photo.jpg" alt="">
<div class="card__overlay"></div>
<div class="card__text">
<h2>Card Title</h2>
<p>A short description</p>
</div>
</div>
.card {
display: grid;
grid-template: 1fr / 1fr;
height: 280px;
border-radius: 10px;
overflow: hidden;
}
.card > * { grid-column: 1; grid-row: 1; }
.card__image {
z-index: 1;
width: 100%; height: 100%;
object-fit: cover;
}
.card__overlay {
z-index: 2;
background: linear-gradient(to top, rgba(0,0,0,.8) 0%, transparent 55%);
}
.card__text {
z-index: 3;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 20px;
color: #fff;
}
grid-template-areas with these zones: hero (spans all 3 columns, 2 rows), lead (column 1, rows 3–4), featured (columns 2–3, row 3), aside (column 2, row 4), promo (column 3, row 4). Then write a media query for screens under 700px that collapses it to a single column in reading order: hero → lead → featured → aside → promo..magazine {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
grid-template-areas:
"hero hero hero"
"hero hero hero"
"lead featured featured"
"lead aside promo";
}
.hero { grid-area: hero; }
.lead { grid-area: lead; }
.featured { grid-area: featured; }
.aside { grid-area: aside; }
.promo { grid-area: promo; }
@media (max-width: 700px) {
.magazine {
grid-template-columns: 1fr;
grid-template-areas:
"hero"
"lead"
"featured"
"aside"
"promo";
}
}