029: CSS Grid
Learning outcomes
By the end of this lesson, you should be able to identify grid containers, tracks, lines, cells, and areas; define explicit columns and rows; use gaps and fr; place items; build intrinsically responsive tracks with minmax() and auto-fit; and decide when Grid is a better fit than Flexbox because the layout depends on two-dimensional alignment.
Prerequisites and retrieval
Recall that wrapping Flexbox lays out each line independently. Now think of a design in which headings, images, and actions need to line up across both rows and columns. Grid controls those two dimensions together while leaving the document's semantic source order intact.
Terminology
- Grid container: An element with
display: gridthat establishes grid formatting for its contents. — Source: CSS Grid 2 - Grid item: An in-flow direct child of a grid container. — Source: CSS Grid 2: Grid items
- Track: The space between two adjacent grid lines, either a row or a column. — Source: CSS Grid 2: Tracks
- Grid line: A numbered boundary that delimits tracks and can be used for placement. — Source: CSS Grid 2: Grid lines
- Cell: The intersection of one row track and one column track. — Source: CSS Grid 2: Cells
- Area: A rectangular region spanning one or more cells that can be used as a placement target. — Source: CSS Grid 2: Areas
- Explicit grid: Tracks declared with
grid-template-columnsorgrid-template-rows. — Source: CSS Grid 2 - Implicit grid: Tracks Grid creates automatically for items that extend beyond the explicit grid. — Source: CSS Grid 2: Implicit tracks
fr: A flexible-length unit that distributes leftover space proportionally among tracks. — Source: CSS Grid 2: fr unitminmax(): A track-sizing function that clamps a track between minimum and maximum bounds. — Source: CSS Grid 2: minmax()- Gap (
gap): "The gutter between grid rows and columns (row-gap / column-gap)." — Source: CSS Grid Level 2: Gutters - Auto-placement: "The algorithm that places grid items automatically when not explicitly placed." — Source: MDN: Auto-placement in grid
Mental model: define tracks, then place content
Think of Grid as a coordinate system. Its direct children occupy cells and can span from one line to another. Unlike Flexbox, a Grid track extends across the grid, so items in separate rows can share the same column geometry. Content still contributes intrinsic minimum sizes, and 1fr does not mean exactly the same thing as a percentage.
That makes Grid a good choice for page regions, galleries, and repeated cards whose relationships matter in both axes. Flexbox is usually the clearer choice for navigation, toolbars, and distribution along one main axis. The two models can be nested: Grid can arrange the cards while Flexbox arranges the contents of one card. Unless you change it, an item still lays out its own contents using normal flow.
Beginner example: explicit portfolio grid
<section class="projects" aria-labelledby="projects-title">
<h2 id="projects-title">Selected work</h2>
<article class="project-card">...</article>
<article class="project-card">...</article>
<article class="project-card">...</article>
</section>
.projects {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.projects > h2 {
grid-column: 1 / -1;
margin-block-end: 0;
}
.project-card {
padding: 1rem;
border: 1px solid rgb(203 213 225);
background: white;
}
The heading starts at line 1 and ends at the final line, written as -1. The two equal flexible tracks divide the available space, while minmax(0, 1fr) explicitly permits a track to shrink below its content-based automatic minimum. The content still has to be able to wrap appropriately. At a narrow width, two columns may be cramped. Do not add the responsive breakpoint yet: first make the explicit grid understandable, then let 031 introduce a content-driven breakpoint.
Use DevTools' Grid overlay to display the line numbers. Temporarily give one card grid-column: span 2 and predict where it will land. Restore the declaration unless a featured card that spans the grid is actually part of the design.
Intermediate example: intrinsic auto-fitting card grid
Many card grids can respond to their available width without a media query:
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1.25rem;
}
Read that declaration from the inside out. min(100%, 18rem) keeps the minimum track from being wider than a narrow container. minmax(..., 1fr) then lets each track grow. auto-fit creates as many tracks as fit and collapses empty ones, allowing the cards that remain to stretch. That is intrinsic responsive layout: the available space and a meaningful minimum card width determine the number of columns.
Inside each card, use normal flow or the column Flexbox from 028:
.project-card {
display: flex;
flex-direction: column;
}
.project-card__link { margin-block-start: auto; align-self: flex-start; }
Grid manages the relationships between cards. Flexbox manages vertical distribution within an individual card.
Optional advanced example: named page areas
<div class="portfolio-layout">
<main class="portfolio-layout__main">...</main>
<aside class="portfolio-layout__aside">...</aside>
</div>
.portfolio-layout {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(14rem, 1fr);
grid-template-areas: "main aside";
gap: 2rem;
align-items: start;
}
.portfolio-layout__main { grid-area: main; }
.portfolio-layout__aside { grid-area: aside; }
The names make the intent of each region easy to read. A narrow-screen variation belongs in 031. Keep main before aside in the HTML so the source order remains sensible even without Grid. Also remember that a named area must form a rectangle; Grid cannot name an L-shaped region.
Mistakes, debugging, and DevTools
- Putting grid declarations on the items instead of on the intended container.
- Forgetting that only direct children become grid items.
- Using
1fr 1frwithout checking min-content overflow; inspect the tracks and considerminmax(0, 1fr). - Hard-coding many line positions even though the content changes.
- Using Grid to move content visually away from its DOM order.
- Adding empty HTML elements just to act as spacers; use gaps or tracks instead.
- Assuming auto-placement can repair an illogical source sequence.
- Using explicit fixed columns on narrow screens without a strategy for collapsing them.
Turn on the Grid overlay to inspect tracks, gaps, line numbers, and areas. If an item appears in an unexpected place, inspect the implicit tracks as well. Toggle between auto-fit and auto-fill: auto-fill preserves potential empty tracks, whereas auto-fit collapses them. Test long words and images too, because intrinsic content can widen tracks.
Accessibility and performance
Grid placement changes where an element appears visually; it does not change the DOM's reading or focus sequence. Write the source order logically first, and avoid assigning distant explicit positions to interactive elements. Content must be able to enlarge and reflow, so never use fixed row heights for text. Reserve local scrolling for content that is genuinely two-dimensional.
Ordinary grids perform well. Very large explicit grids and animations that repeatedly change layout can cost more. Use responsive image files as well; a polished grid is not a reason to download oversized thumbnails.
Deep dive: explicit and implicit grids
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto auto;
}
These declarations define the explicit grid. When more items need more rows, Grid creates implicit tracks for them.
Control the size of those automatically created rows with:
.grid {
grid-auto-rows: minmax(8rem, auto);
}
For auto-placement that proceeds by column instead of by row:
.grid {
grid-auto-flow: column;
}
Most layouts should retain natural, row-wise auto-placement. Change it only when the content model clearly calls for column-oriented placement.
Deep dive: auto-fit versus auto-fill
Both functions can produce a responsive list of tracks:
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}
auto-fill keeps empty potential tracks in the grid. auto-fit collapses empty tracks, giving the existing items room to expand. With enough items, the two often look identical, so compare them with fewer items than the available maximum number of tracks.
Worked example: product grid with featured item
<section class="products">
<article class="product product--featured">...</article>
<article class="product">...</article>
<article class="product">...</article>
<article class="product">...</article>
</section>
.products {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: 1rem;
}
.product--featured {
grid-column: span 2;
}
The featured span is safe only when the grid has at least two columns. A more resilient approach applies it within a container or media condition that guarantees enough room:
@media (min-width: 48rem) {
.product--featured {
grid-column: span 2;
}
}
Deep dive: placement with named lines and areas
Named areas make a page-level arrangement readable:
.page {
display: grid;
grid-template:
"header header" auto
"main aside" 1fr
"footer footer" auto
/ minmax(0, 1fr) 18rem;
gap: 1.5rem;
}
.page__header { grid-area: header; }
.page__main { grid-area: main; }
.page__aside { grid-area: aside; }
.page__footer { grid-area: footer; }
Named lines are often a better fit when the design is organized around tracks:
.layout {
display: grid;
grid-template-columns:
[full-start] minmax(1rem, 1fr)
[content-start] minmax(0, 70rem)
[content-end] minmax(1rem, 1fr)
[full-end];
}
A child can use those lines to span the full bleed area while other content stays aligned to the central track.
Deep dive: alignment in Grid
Grid has two alignment dimensions, and each has a property for the items and for the grid as a whole:
.grid {
justify-items: stretch; /* inline axis within each area */
align-items: start; /* block axis within each area */
justify-content: center;/* whole grid inside container */
align-content: start; /* whole grid in block axis */
}
An individual item can override the item-level defaults:
.item {
justify-self: end;
align-self: center;
}
Do not confuse alignment inside an area with distribution of the tracks themselves.
Worked example: dashboard using minmax() safely
.dashboard {
display: grid;
grid-template-columns:
minmax(0, 2fr)
minmax(16rem, 1fr);
gap: 2rem;
}
.dashboard__main {
min-inline-size: 0;
}
The 0 minimum prevents content-based track minimums from unexpectedly forcing horizontal overflow.
Deep dive: dense auto-placement caution
.gallery {
grid-auto-flow: dense;
}
dense can fill an earlier visual hole with a later item. As a result, visual order may no longer match source order. Use it only for non-sequential decorative galleries where that difference cannot harm reading, keyboard navigation, or comprehension.
Preview: subgrid
Modern Grid can allow a nested grid to participate in its parent's track sizing:
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3;
}
Subgrid is useful for repeated cards whose title, body, and action rows should align across the parent grid. It deserves a deeper treatment in the modern CSS lesson after the roadmap-completion section.
Tiered exercises
Checkpoint: read a grid declaration inside out
For repeat(auto-fit, minmax(min(100%, 18rem), 1fr)), begin with the innermost minimum. It evaluates to 18rem when the container is wide enough, but it can never exceed 100% of a narrower container. Each track can then grow to a fraction of the remaining grid space. Finally, auto-fit repeats as many tracks as fit and collapses empty ones. Reading nested functions in this order turns a declaration that looks magical into a sequence of understandable decisions.
Compare it with auto-fill in a wide grid that is empty or has only a few cards. auto-fill retains room for potential empty tracks; auto-fit collapses those tracks so the existing cards stretch. Neither option is always right. A card gallery often benefits from auto-fit, while a design that needs to preserve potential column slots may deliberately use auto-fill.
Test intrinsic minimums by placing a long, unbroken token in one card. A track declared as 1fr has an automatic minimum that may respect min-content sizing and force overflow. minmax(0, 1fr) lets the track shrink, but the content still needs overflow-wrap, local scrolling, or a semantic opportunity to break. The grid constraint and the content-handling decision are separate, just as they are in Flexbox.
Next, compare Grid and Flexbox with six cards. A wrapping Flexbox line distributes its own space independently, so the final line can have different item widths. Grid shares its column tracks across rows, keeping cards aligned vertically and horizontally. Choose between them based on the relationship the layout needs, not on which syntax is newer.
Placement cannot repair source order. Set one card to grid-column: 2 and another to an earlier empty cell, then Tab through their links. The visual order can differ from sequential focus order. Remove those declarations and keep a meaningful DOM sequence. A featured span is safe when it does not create an order mismatch and still fits at narrow widths.
Now inspect implicit rows. Add more cards than you originally planned; Grid should auto-place them without overlap. Avoid fixed row heights because descriptions vary and enlarged text needs room to grow. When the presentation needs consistent alignment, use align-items and an internal card layout rather than clipping the tracks.
Named lines can clarify repeated structures without named areas:
.article-grid {
display: grid;
grid-template-columns: [content-start] minmax(0, 2fr) [content-end aside-start] minmax(14rem, 1fr) [aside-end];
gap: 2rem;
}
For a small portfolio, this is optional and more verbose than necessary. Name lines when the names make placement intent easier for the team to understand; line numbers are sufficient for simple spanning.
Alignment operates at two levels. justify-items and align-items set defaults inside grid areas. justify-content and align-content distribute the tracks when the grid is smaller than its container. If changing align-content appears to have no effect, there may be no extra block-axis space to distribute. As with Flexbox, inspect the geometry before adding a fixed height.
Grid areas must be rectangular, so a template string cannot define an L-shaped named area. Keep the narrow-screen source order logical, then introduce named-area columns only at a justified query. Whenever two areas exchange visual positions, verify the keyboard order; in most cases, preserving the same sequence everywhere is the safer solution.
Finish with a content mutation: remove one card and add five. Auto-placement and intrinsic tracks should still produce a coherent gallery. A grid that works only with exactly six cards describes a demo, not a reusable layout system.
Foundation: Create a two-column project grid with equal tracks and a heading spanning both columns. Inspect the line numbers.
Core: Replace the fixed tracks with an intrinsic auto-fit grid. Cards should prefer at least 18rem, but the grid must not overflow a smaller container.
Stretch: Build a main/aside named-area layout for a wide container. Keep the DOM order correct and explain how the layout should collapse later.
.projects { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.projects > h2 { grid-column: 1 / -1; }
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1.25rem;
}
.portfolio-layout {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(14rem, 1fr);
grid-template-areas: "main aside";
gap: 2rem;
align-items: start;
}
.portfolio-layout__main { grid-area: main; }
.portfolio-layout__aside { grid-area: aside; }
Recap and exit questions
Grid defines shared rows and columns. Tracks, line placement, spanning, gaps, and intrinsic track functions express two-dimensional relationships, while content and source order remain central to the result.
- How does a grid item differ from a descendant?
- What does
1 / -1mean? - Why use
minmax(0, 1fr)? - How do
auto-fitandminmax()create intrinsic responsiveness? - Which part of a card gallery belongs to Grid, and which part might belong to Flexbox?
