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

CSS Grid — The Complete Guide

Two-dimensional layout done right — grid-template-columns/rows, grid areas, and the mental model that makes Grid click.

50 min August 2026
// Part 01 — The Two-Dimensional Mental Model

display: grid — Thinking in Rows AND Columns at Once

Every layout tool you have used so far in this track — normal document flow, floats, Flexbox — is fundamentally one-dimensional. Flexbox lays items out along a single axis (a row, or a column) and lets the cross axis take care of itself, item by item. CSS Grid is different in one specific, load-bearing way: it lets you define rows and columns at the same time, as a single coordinate system, and then place content anywhere inside that grid — including deliberately out of source order. That is the entire idea Grid exists to solve. If you have ever tried to build a page layout in Flexbox and found yourself fighting to get a sidebar, header, and footer to all line up against a shared set of column and row boundaries, that fight is exactly what Grid was built to end.

Turning an element into a grid container
.layout {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: 80px 1fr 60px;
  gap: 16px;
}

Setting display: grid on an element makes it a grid container — every direct child automatically becomes a grid item, without needing any property set on the children themselves (unlike Flexbox, where you at least conceptually think about each child's flex behaviour). grid-template-columns and grid-template-rows define the actual track sizes: the layout above creates three columns (200px, a flexible middle column, 200px) and three rows (80px, a flexible middle row, 60px) — a skeleton that reads almost exactly like a hand-drawn wireframe.

A minimal three-column layout
<div class="layout">
  <header>Header</header>
  <main>Main content</main>
  <aside>Sidebar</aside>
</div>

By default, grid items are placed automatically into the grid, one per cell, in source order, filling row by row — the same instinctive "just drop them in" behaviour you get from normal block flow, except now flowing across a real two-dimensional grid instead of a single top-to-bottom column. Explicit placement (Part 05) is what lets you break out of that automatic order deliberately.

💡 Note
gap (formerly written as the vendor-specific grid-gap, which is now just a legacy alias) puts real space between grid tracks without adding margin to individual items — meaning there is no lingering space at the outer edges of the grid the way margin-based spacing between Flexbox items always leaves behind. You can also set row-gap and column-gap independently if the spacing needs differ between the two axes.

inline-grid — the rarely-needed sibling

Just as Flexbox has inline-flex, Grid has display: inline-grid — the grid container itself behaves like an inline-level box in the surrounding layout, while everything inside it still lays out as a grid. This is genuinely rare in practice; the vast majority of real Grid usage is display: grid on a block-level container.

// Part 02 — The fr Unit

fr — The Unit That Only Exists for Grid

fr stands for "fraction" — a unit that represents a share of the leftover space in the grid container, after every fixed-size track (pixels, rems, percentages) has already been subtracted. It is the single idea that makes Grid track sizing feel effortless once it clicks, and genuinely confusing before it does.

Equal thirds — the simplest possible fr layout
.grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}
/* Three equal columns, each getting exactly one third of the available width.
   There is no leftover space to distribute unevenly — all three "shares" are 1. */
Mixing fixed tracks with fr — the pattern you will use constantly
.layout {
  display: grid;
  grid-template-columns: 250px 1fr;
  /* The sidebar is a fixed 250px, no matter the container width.
     The 1fr column gets EVERYTHING that's left over — not "the rest divided by 1",
     but literally "100% of whatever remains after 250px is subtracted." */
}

The genuinely important detail: fr distributes space proportionally among fr tracks only, after fixed tracks are already accounted for. grid-template-columns: 1fr 2fr 1fr does not mean "25%, 50%, 25% of the container" in the way percentages would — it means "whatever space is left after gaps and fixed tracks, split it 1 part : 2 parts : 1 part." The middle column ends up exactly twice as wide as each outer one, but the actual pixel values shift as the container resizes.

Why fr beats percentage-based columns for this specific job
/* Percentages: technically works, but gap has to be manually subtracted
   from somewhere, or the columns overflow the container. */
.old-way {
  display: grid;
  grid-template-columns: 33.33% 33.33% 33.33%;
  gap: 16px; /* this gap is NOT accounted for in the 33.33% figures — overflow risk */
}

/* fr: gap is subtracted automatically before the fr units are calculated.
   No manual math, no overflow. */
.grid-way {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 16px;
}
🎯 Pro Tip
A genuinely common real pattern: grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) instead of a bare 1fr 1fr. A bare fr track has an implicit minimum width based on its content (min-width: auto), which means a long unbreakable string or a wide image inside a grid item can force that track wider than its fair fr share — wrapping it in minmax(0, 1fr) overrides that implicit minimum and lets the track actually shrink to its fr share. This exact fix — "my grid column won't shrink even though I gave it 1fr" — is one of the most searched CSS Grid problems that exists.
// Part 03 — repeat() and minmax()

repeat() and minmax() — Writing Less, Describing More

Writing out 1fr 1fr 1fr 1fr 1fr 1fr for a six-column grid works, but it does not scale, and it does not communicate intent. repeat() lets you express "N tracks of this size" directly.

repeat() — the same six columns, expressed properly
.grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  /* Identical to: 1fr 1fr 1fr 1fr 1fr 1fr */
}

.grid-mixed {
  display: grid;
  grid-template-columns: 200px repeat(3, 1fr) 100px;
  /* repeat() can appear alongside other track definitions, not just alone */
}

minmax(min, max) defines a track that is never smaller than its minimum and never larger than its maximum — genuinely flexible sizing within real, explicit bounds, rather than either a fixed size or unlimited growth.

minmax() — a column that flexes between two real limits
.card-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(200px, 1fr));
  /* Each column is at least 200px, but grows to share the leftover space
     equally (1fr) if the container is wide enough to give it more than 200px. */
}

Combined, repeat() and minmax() produce the single most useful line in all of CSS Grid — the pattern that builds a genuinely responsive grid with zero media queries.

The auto-fill + minmax() combo — a self-wrapping responsive grid
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 20px;
}
/* Reads as: "Fit as many 220px-minimum columns as will comfortably fit,
   then let each one grow to fill any leftover space." As the viewport
   shrinks, columns wrap down automatically — no breakpoint needed. */

This deserves its own full treatment — the difference between auto-fill and auto-fit, and a real image gallery built on exactly this pattern, is covered in depth in the next module, CSS Grid in Practice. For now, the important thing to internalise is that repeat() accepts auto-fill/auto-fit as its count argument instead of a fixed number — that is what makes the column count itself responsive, not just the column widths.

⚠️ Important
minmax(200px, 1fr) is very different from minmax(1fr, 200px) — the arguments are not interchangeable. The first argument must always be the smaller bound and the second the larger; reversing them (a fr unit as the minimum, a pixel value as the maximum) is invalid and the browser drops the entire declaration.
// Part 04 — grid-template-areas

grid-template-areas — Drawing Your Layout in the CSS Itself

grid-template-areas is genuinely unlike anything else in CSS: you name regions of the grid, then draw the layout as an actual ASCII-art-style grid of those names directly in your stylesheet. It reads like a wireframe because it effectively is one.

Naming and drawing a layout
.layout {
  display: grid;
  grid-template-columns: 220px 1fr;
  grid-template-rows: 70px 1fr 60px;
  grid-template-areas:
    "sidebar header"
    "sidebar main"
    "sidebar footer";
}

header  { grid-area: header;  }
main    { grid-area: main;    }
footer  { grid-area: footer;  }
aside   { grid-area: sidebar; }

Each child is assigned to a named region with grid-area, and the container's grid-template-areas string literally draws where each region sits — the sidebar spans all three rows in the example above simply because "sidebar" appears in every row of the drawing. There is no separate row/column-span property needed for this case; the shape of the ASCII drawing itself is the spanning logic.

🎯 Pro Tip
Every row in a grid-template-areas string must have the same number of cells, and every named area must form a single, unbroken rectangle. You cannot draw an L-shape or a region with a gap in the middle — if a name appears in a non-rectangular arrangement, the declaration is invalid and the browser rejects the entire property. Use . (a period) for a cell that is deliberately empty — an intentional gap in the grid that no item occupies.
Using . for an intentionally empty cell
.layout {
  grid-template-columns: 220px 1fr 1fr;
  grid-template-areas:
    "sidebar header header"
    "sidebar .      widget"
    "sidebar footer footer";
}
/* The middle cell in row 2 is deliberately empty — no item is placed there. */

Why this earns its place over grid-column/grid-row for real page layouts

You could achieve an identical result with numeric line placement (Part 05) — but grid-template-areas is dramatically more readable for anyone maintaining the layout later, since the CSS visually mirrors the actual page structure. This is why it is the dominant pattern for full-page, semantically distinct layouts (header/nav/main/aside/footer), while numeric line placement tends to be reached for inside smaller, more repetitive components like card grids, where naming every cell would be excessive ceremony for little benefit.

// Part 05 — Line-Based Placement

grid-column / grid-row — Placing Items by Line Number

Every grid has numbered grid lines — not tracks, the lines between tracks. A 3-column grid has 4 vertical grid lines (numbered 1 through 4, left to right); a 3-row grid has 4 horizontal grid lines. grid-column and grid-row place an item by specifying which lines it starts and ends at — this is the mechanism that lets an item span multiple tracks, or be placed somewhere other than the browser's automatic next-available cell.

Placing and spanning items by line number
.grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(2, 100px);
  gap: 12px;
}

.hero {
  grid-column: 1 / 3;   /* start at line 1, end at line 3 — spans 2 columns */
  grid-row: 1 / 2;
}

.wide-banner {
  grid-column: 1 / -1;  /* -1 always means "the last line" — spans the FULL width,
                            regardless of how many columns the grid actually has */
}

grid-column: 1 / 3 is shorthand for grid-column-start: 1; and grid-column-end: 3; — the same relationship margin has to margin-top/right/bottom/left. The end line is exclusive of nothing in particular — it is just "the line the item's edge touches," so an item spanning from line 1 to line 3 covers exactly two column tracks (the space between lines 1–2 and 2–3).

The span keyword — an alternative to specifying both lines
.card {
  grid-column: span 2;   /* "start wherever the auto-placement algorithm puts me,
                             but occupy 2 columns from there" — no explicit start line needed */
}

span N is genuinely the more common real-world pattern for grids where items still flow automatically but occasionally need to be wider — a "featured" card in an otherwise uniform product grid, for example — since it does not require calculating exact line numbers by hand, which becomes fragile the moment the column count changes.

💡 Note
Negative line numbers count from the end of the explicit grid, regardless of how many tracks it has: -1 is always the last line, -2 is one before that, and so on. This is what makes grid-column: 1 / -1 a genuinely robust "always span the full width of the grid" pattern — it works correctly even if the number of columns changes later, unlike hardcoding an end line number that assumes a specific column count.
// Part 06 — Implicit vs Explicit Grid

The Explicit Grid You Define, and the Implicit Grid the Browser Invents

The explicit grid is exactly the tracks you defined with grid-template-columns and grid-template-rows. The implicit grid is what the browser silently creates when content needs more rows or columns than you explicitly defined — a detail that catches nearly every developer off guard the first time it happens.

More items than defined rows — the implicit grid kicks in
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: 100px 100px;   /* only 2 rows explicitly defined */
}
/* 9 items, 3 columns → 3 rows are needed, but only 2 were defined.
   The browser creates a 3rd row IMPLICITLY, and — critically — that
   implicit row does NOT get the 100px height from grid-template-rows.
   By default, it sizes to fit its content instead. */

This is the single most common source of "why is my last row a different height than the others" bugs in real Grid layouts. The fix is grid-auto-rows (and its column equivalent, grid-auto-columns), which sets the size for any track the browser creates implicitly — the counterpart to grid-template-rows, but for rows you did not explicitly name.

Controlling the size of implicitly created tracks
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: 100px 100px;
  grid-auto-rows: 100px;   /* any implicit row created beyond the two explicit
                               ones is ALSO 100px, matching the rest of the grid */
}

grid-auto-flow — controlling how items fill the implicit grid

By default (grid-auto-flow: row), auto-placed items fill row by row, creating new rows as needed. Setting it to column flips that — items fill column by column instead, creating new columns as needed (which requires grid-auto-columns to size those new columns sensibly).

grid-auto-flow: column — a genuinely different fill direction
.grid {
  display: grid;
  grid-template-rows: repeat(2, 100px);
  grid-auto-flow: column;
  grid-auto-columns: 150px;
}
/* Items fill DOWN each column first, then move to the next column —
   the opposite of the default row-first behaviour. */
🎯 Pro Tip
grid-auto-flow: dense is a lesser-known modifier worth knowing: it tells the auto-placement algorithm to backfill earlier empty cells with later items that happen to fit, rather than strictly preserving source order. It is genuinely useful for masonry-style grids with items of mixed spans, where leaving gaps would otherwise look broken — but it comes at the direct cost of visual order no longer matching source (DOM) order, which can be a real accessibility concern for keyboard and screen-reader navigation. Use it deliberately, not by default.
// Part 07 — Aligning Content Within the Grid

justify-items, align-items, and Their content Counterparts

Grid alignment properties split cleanly along the same two axes the rest of Grid is built on, and along a second distinction that trips people up at first: aligning items within their own cell versus aligning the whole grid within its container, when the grid itself is smaller than the container.

Aligning items within their individual cells
.grid {
  display: grid;
  justify-items: center;  /* horizontal alignment of each item inside its own cell */
  align-items: center;    /* vertical alignment of each item inside its own cell */
}

/* Shorthand for both at once: */
.grid {
  place-items: center;   /* align-items then justify-items, in that order */
}
Aligning the grid itself within its container
.grid {
  display: grid;
  grid-template-columns: repeat(3, 100px);  /* a grid narrower than its container */
  justify-content: center;  /* centers the WHOLE grid horizontally in the container */
  align-content: center;    /* centers the WHOLE grid vertically in the container */
}

The naming mirrors Flexbox closely on purpose — justify-* is always the inline (typically horizontal) axis and align-* is always the block (typically vertical) axis in both layout modes. The -items vs -content distinction is the part that is genuinely specific to Grid: -items properties move things inside their own cell; -content properties move the entire set of tracksas a group within the container, and only have a visible effect when the defined tracks do not already fill the container completely.

A single item can also override the container's justify-items/ align-items for itself specifically, using justify-self and align-self — exactly the same "container sets the default, an individual item can opt out" relationship align-self already has in Flexbox.

One item overriding the container default
.grid { justify-items: start; }

.featured-card {
  justify-self: center;   /* this one item centers itself, ignoring the container's "start" */
}
// Part 08 — Real World
💼 What This Looks Like at Work

A Design-System Rebuild at a Portland Analytics Company

Scenario — B2B analytics SaaS, Portland · Dashboard shell rebuild

A front-end engineer at a Portland-based analytics company inherits a dashboard shell built years earlier from floated divs and a handful of absolutely-positioned overrides for the sidebar. Every time a designer requests a layout tweak — widening the sidebar, adding a secondary top bar for filters — the change requires touching three or four unrelated CSS rules, and something else regressed almost every time.

The old shell, roughly reconstructed
.shell {
  position: relative;
}
.sidebar {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  width: 240px;
}
.topbar {
  margin-left: 240px;
  height: 60px;
}
.content {
  margin-left: 240px;
  padding-top: 60px;
  min-height: 100vh;
}
.footer {
  margin-left: 240px;
}
/* Every region's position depends on hardcoded knowledge of every OTHER
   region's size, duplicated across four separate rules. Change the sidebar
   width once, and it has to be updated in three unrelated places. */

The rebuild

The engineer replaces the entire shell with a single grid-template-areas declaration — the exact pattern from Part 04 of this module. The sidebar width now lives in exactly one place.

The rebuilt shell
.shell {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: 60px 1fr auto;
  grid-template-areas:
    "sidebar topbar"
    "sidebar content"
    "sidebar footer";
  min-height: 100vh;
}

.sidebar { grid-area: sidebar; }
.topbar  { grid-area: topbar;  }
.content { grid-area: content; }
.footer  { grid-area: footer;  }

Resizing the sidebar is now a single-number change to grid-template-columns, with zero risk of a stray region silently overlapping another — every region's boundary is defined by the grid itself, not by each region independently guessing the sidebar's width. When a request comes in a week later to add a collapsible sidebar state, the fix is a single class toggle that changes grid-template-columns from 240px 1fr to 60px 1fr, with a transition — no repositioning logic needed anywhere else, because everything downstream of the sidebar already reflows automatically off the grid definition.

// Part 09 — Misconceptions

Five Misconceptions About CSS Grid

✕ ""Grid replaced Flexbox — you should just use Grid for everything now""
Grid and Flexbox solve different-shaped problems, not competing versions of the same problem. Flexbox is genuinely better suited to one-dimensional content (a navbar, a button row, a card's internal content) where items should size based on their content. Grid is built for two-dimensional layout with explicit tracks. The next two modules in this track cover exactly when to reach for each.
✕ ""fr is just a synonym for percentage""
fr distributes only the LEFTOVER space, after fixed-size tracks and gaps are already subtracted — percentages divide the full container width, gap and all, which is exactly why percentage-based columns combined with gap are prone to overflow in a way fr columns are not.
✕ ""grid-template-areas can express any layout shape you can draw""
Every named area must form a single unbroken rectangle. L-shapes, regions with holes, and non-rectangular arrangements are invalid and the entire declaration is silently rejected by the browser — you would need to fall back to numeric grid-column/grid-row placement for genuinely irregular shapes.
✕ ""If I define grid-template-rows, every row in my layout will be that size""
grid-template-rows only sizes the EXPLICIT rows you defined. Any row the browser creates implicitly, because your content needed more rows than you declared, sizes to its content by default instead — unless you also set grid-auto-rows to control implicit track sizing, exactly the bug covered in Part 06.
✕ ""span 2 and grid-column: 1 / 3 always do the same thing""
span 2 means "occupy 2 tracks starting from wherever auto-placement puts me" — it does not pin the item to specific line numbers. grid-column: 1 / 3 pins the item to an exact position regardless of where auto-placement would otherwise have put it. They only produce identical results when the item would have auto-placed at line 1 anyway.
// Part 10 — Interview Prep

6 Interview Questions — With Complete Answers

What does the fr unit actually represent, and how is it different from a percentage?
fr represents a share of the leftover space in a grid container, calculated AFTER fixed-size tracks and gap are already subtracted. A percentage always divides the full container size, gap included, which is why combining percentages with gap risks overflow unless you manually account for the gap — fr handles that subtraction automatically.
Explain the difference between the explicit grid and the implicit grid.
The explicit grid is exactly what you defined with grid-template-columns/grid-template-rows. The implicit grid is made up of extra tracks the browser silently creates when placed content needs more rows or columns than were explicitly defined. Implicit tracks size to their content by default; grid-auto-rows and grid-auto-columns let you control that sizing instead of leaving it to content-based defaults.
Why would you choose grid-template-areas over numeric grid-column/grid-row placement?
grid-template-areas lets you literally draw the layout shape in the CSS, which is dramatically more readable and maintainable for a full page layout with semantically distinct regions. Numeric line placement is more appropriate for smaller, repetitive components (like a card grid with an occasional wide item) where naming every region would be more ceremony than it is worth.
What is the difference between auto-fill and auto-fit inside repeat(), and why does it matter with a small number of items?
Both fit as many tracks of the given minmax() size as will comfortably fit the container. auto-fill preserves empty tracks as real (collapsed but present) tracks if there are fewer items than would fill a row, which can leave visible empty space when combined with certain alignment. auto-fit collapses those empty tracks to zero width, letting the actual content tracks stretch to fill the row instead. The difference is invisible with enough items to fill every row, and very visible with too few.
What is the difference between justify-items and justify-content on a grid container?
justify-items controls how EACH item aligns within its own individual cell. justify-content controls how the entire set of grid tracks aligns within the container as a group, and only has a visible effect when the defined tracks do not already fill the container completely — for example, a grid of fixed-width columns inside a wider container.
Why might minmax(0, 1fr) be used instead of a bare 1fr for grid columns holding text or images?
A bare fr track still has an implicit minimum width equal to the size of its content (min-width: auto), so a long unbreakable string or a wide image can force that column wider than its fair fr share, breaking the intended proportions. minmax(0, 1fr) explicitly overrides that implicit minimum to zero, letting the track actually shrink down to its fr share and forcing the content to wrap or scroll within it instead.
// Common Mistakes

CSS Grid Mistakes Beginners Make Constantly

Before
.grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}
.item { width: 400px; } /* fighting the grid track size directly */
After
.grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}
/* Don't set a fixed width on a grid ITEM to control its track size —
   size the TRACK itself in grid-template-columns instead. */
Setting width directly on a grid item to control its size fights the grid instead of using it — the track sizing (grid-template-columns) is the correct place to control column width, not per-item CSS.
Before
.layout {
  display: grid;
  grid-template-areas:
    "header header"
    "sidebar main";
}
main { grid-area: content; } /* typo — "content" was never defined */
After
.layout {
  display: grid;
  grid-template-areas:
    "header header"
    "sidebar main";
}
main { grid-area: main; } /* matches the name used in grid-template-areas exactly */
grid-area must match a name that actually appears in grid-template-areas, character for character. A mismatched name is not an error the browser reports loudly — the item simply falls back to auto-placement, silently landing somewhere unexpected.
Before
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}
.item {
  grid-column: 1 / 4;  /* meant to span 3 columns... */
  grid-row: 1;
}
After
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}
.item {
  grid-column: 1 / 4;  /* correct — a 3-column grid has 4 lines (1,2,3,4),
                           so spanning all 3 columns really is "1 / 4" */
}
This one is actually correct — it is included because it is the single most common off-by-one confusion with Grid: a grid with N columns has N+1 lines. Beginners frequently write grid-column: 1 / 3 expecting to span 3 columns, when that actually spans only 2.
Before
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, 220px);  /* no minmax() at all */
}
After
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
repeat(auto-fill, 220px) fits as many fixed 220px columns as possible, but leaves leftover space unfilled at the end of each row instead of letting the last row of columns stretch — minmax(220px, 1fr) is what actually makes columns grow to fill the remaining space.
// Error Library

Errors and Rendering Bugs You Will Hit With Grid — And Exactly Why

An item silently doesn't appear where grid-area says it should — no console error at all
Cause: The grid-area name on the item does not exactly match a name used in the container's grid-template-areas string (a typo, a casing mismatch, or a leftover name from a refactor). Grid does not raise a console error for this — the item just falls back to automatic placement.
Fix: Double check the exact spelling of the name in both grid-area on the item and grid-template-areas on the container. Browser DevTools' Grid inspector (available in Chrome, Firefox, and Safari) will visually overlay the defined areas and immediately reveal the mismatch.
The entire grid-template-areas declaration appears to do nothing
Cause: One row in the ASCII-art string has a different number of cells than the others, or a named region does not form a single unbroken rectangle — both make the whole declaration invalid, and CSS drops invalid declarations entirely rather than partially applying them.
Fix: Check that every quoted row has the same number of space-separated names, and that each name's cells form one contiguous rectangular block with no gaps or L-shapes. Use "." for intentionally empty cells rather than omitting them.
A grid column refuses to shrink below its content's width, even though it is set to 1fr
Cause: fr tracks have an implicit minimum size of min-width: auto, meaning the track will not shrink smaller than its largest piece of unbreakable content (a long word, a wide image) — the 1fr only governs how the LEFTOVER space is divided, not the absolute floor.
Fix: Set an explicit minimum with minmax(0, 1fr) instead of a bare 1fr, which overrides the implicit content-based minimum and lets the track shrink freely.
The last row of a grid is a visibly different height than the rest
Cause: More items were placed than the explicitly defined grid-template-rows accounts for, so the browser created an implicit row for the overflow — and implicit rows size to their content by default, ignoring grid-template-rows entirely.
Fix: Set grid-auto-rows to the same size as your explicit rows, so any implicitly created row matches the rest of the grid.
Console warning: "Invalid property value" (on a minmax() declaration)
Cause: The arguments to minmax() were given in the wrong order — a maximum value first, a minimum value second (e.g. minmax(1fr, 200px)), which is not a valid range.
Fix: Always put the smaller/minimum bound first: minmax(200px, 1fr), not minmax(1fr, 200px).

🎯 Key Takeaways

  • CSS Grid defines rows and columns simultaneously as a single coordinate system — the defining difference from every one-dimensional layout tool you have used so far, including Flexbox.
  • fr distributes leftover space AFTER fixed tracks and gap are subtracted — it is not a percentage, and gap does not need to be manually accounted for the way it does with percentage-based tracks.
  • repeat(auto-fill, minmax(min, 1fr)) builds a genuinely responsive grid with zero media queries — it fits as many minimum-sized columns as comfortably fit, then lets them grow to fill any leftover space.
  • grid-template-areas lets you literally draw your layout in CSS — every named region must form a single unbroken rectangle, and every row of the ASCII string needs the same cell count, or the whole declaration is invalid.
  • grid-column/grid-row place items by line number, not track number — an N-column grid has N+1 lines. span N is usually the more robust choice over exact line numbers for items that should still mostly auto-place.
  • The implicit grid is created automatically whenever content needs more tracks than you explicitly defined — those implicit tracks size to their content unless grid-auto-rows/grid-auto-columns say otherwise.
  • justify-items/align-items (or place-items) align content WITHIN each cell; justify-content/align-content align the entire grid AS A GROUP within its container — the two only look identical when the grid exactly fills its container.

What comes next

Now that the full Grid vocabulary is in place, the next module puts every piece of it to work on real layouts — a holy grail page shell, a responsive image gallery, and a genuinely non-trivial dashboard.

Next Module → CSS Grid in Practice — Real Layouts
Share

Discussion

0

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

Continue with GitHub
Loading...