Lesson 9
CSS Grid
Grid is CSS's two-dimensional layout system. Where Flexbox handles one axis at a time, Grid controls both columns and rows simultaneously. It's designed for page-level layouts, complex component arrangements, and any situation where you need items placed precisely on a two-dimensional plane.
The mental model
Think of Grid as a spreadsheet you define in CSS. You declare how many columns and rows you want, how big they are, and then either let items flow into cells automatically or place them explicitly by referencing line numbers or named areas.
Key terminology:
- Grid container - the element with
display: grid - Grid item - direct children of the container
- Grid line - the dividing lines between columns/rows (numbered starting from 1)
- Grid track - a column or row (the space between two adjacent lines)
- Grid cell - the intersection of one column track and one row track
- Grid area - a rectangular region spanning one or more cells
The core difference from Flexbox: Flexbox items compete for space along a single axis. Grid items are placed into a pre-defined structure. Grid is "layout first, content fills it." Flexbox is "content first, layout adapts to it."
Defining columns and rows
The two fundamental properties are grid-template-columns and grid-template-rows. They define the track sizes:
3 columns (150px, 1fr, 1fr) × 2 rows (auto)
.container {
display: grid;
grid-template-columns: 150px 1fr 1fr;
grid-template-rows: auto auto;
gap: 0.5rem;
}
Track sizes can be:
- Fixed lengths -
150px,10rem - Percentages -
25% - The
frunit - a fraction of the remaining free space auto- size to content (but will stretch if there's extra space)min-content/max-content- intrinsic sizing keywords
The fr unit
fr stands for "fraction of remaining space." After fixed tracks are laid out, the leftover space is divided among fr tracks proportionally:
1fr 2fr 1fr - middle column gets double the free space
.container {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
/* Total fr = 4. Column 1 gets 1/4, column 2 gets 2/4, column 3 gets 1/4 */
}
fr vs percentage: fr divides remaining space (after fixed tracks and gaps are subtracted). Percentages divide the total container width (and can overflow if gaps/fixed tracks exist). Prefer fr in most cases.
repeat() shorthand
When you have many equal-sized tracks, repeat() avoids repetition:
repeat(4, 1fr) - four equal columns
/* These are equivalent: */
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-template-columns: repeat(4, 1fr);
/* You can repeat patterns: */
grid-template-columns: repeat(3, 1fr 2fr);
/* Produces: 1fr 2fr 1fr 2fr 1fr 2fr (6 columns) */
auto-fill and auto-fit
The magic of responsive Grid without media queries. Instead of specifying a fixed count, let the browser figure out how many columns fit:
auto-fill - as many 120px-minimum columns as fit (empty tracks remain)
auto-fit - same as above but empty tracks collapse (items stretch to fill)
/* auto-fill: creates as many tracks as fit, keeps empty ones */
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
/* auto-fit: creates as many tracks as fit, collapses empty ones */
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
When to use which: auto-fit is what you usually want - items stretch to fill the row. auto-fill is for when you want consistent column widths even if items don't fill the row (the empty columns hold their space).
Placing items explicitly - grid lines
By default, items flow into the grid automatically (like text fills cells in a spreadsheet). But you can place items precisely using line numbers:
Item A spans columns 1 - 3, item B is placed at row 2 column 2 - 4
/* Place by line numbers */
.item-a {
grid-column: 1 / 3; /* start at line 1, end at line 3 (spans 2 columns) */
grid-row: 1 / 2; /* row 1 only */
}
/* Shorthand with span */
.item-b {
grid-column: span 2; /* span 2 columns from wherever auto-placed */
}
/* Negative lines count from the end */
.full-width {
grid-column: 1 / -1; /* from first line to last line (full width) */
}
Line numbering: A 3-column grid has 4 column lines (1, 2, 3, 4). Lines are the borders between tracks, not the tracks themselves. grid-column: 1 / 3 means "from line 1 to line 3" - spanning 2 tracks.
Named template areas
For page layouts, named areas are the most readable approach. You literally draw your layout in ASCII:
Holy grail layout with named areas
.layout {
display: grid;
grid-template-columns: 1fr 3fr 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header header"
"sidebar main aside"
"footer footer footer";
min-height: 100vh;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.aside { grid-area: aside; }
.footer { grid-area: footer; }
/* Responsive: stack on mobile */
@media (max-width: 768px) {
.layout {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
}
}
Why named areas are powerful: You can completely restructure a layout by changing only grid-template-areas and grid-template-columns in a media query. The items don't change - just where they appear. This is a fundamentally different approach to responsive design compared to reordering DOM elements.
The implicit grid
When items are placed beyond the explicit grid (the tracks you defined), the browser creates additional tracks automatically. This is the implicit grid.
Only 2 columns defined, but 5 items - extra rows are created implicitly
.container {
display: grid;
grid-template-columns: repeat(2, 1fr); /* explicit: 2 columns */
grid-auto-rows: 60px; /* implicit rows get 60px height */
}
/* Control implicit track sizing: */
grid-auto-rows: minmax(60px, auto); /* at least 60px, grow to fit content */
grid-auto-columns: 200px; /* implicit columns get 200px */
grid-auto-flow - controlling placement direction
By default, items fill row by row. You can change this to column-first (like a newspaper layout):
grid-auto-flow: column - items fill columns first
/* Default: items flow into rows */
grid-auto-flow: row;
/* Items flow into columns instead */
grid-auto-flow: column;
/* Dense packing: fills holes left by explicitly-placed items */
grid-auto-flow: row dense;
dense packing: When some items are explicitly placed and leave gaps, grid-auto-flow: dense lets later items backfill those gaps. Useful for masonry-like layouts, but be aware it can reorder items visually (accessibility concern - screen readers still follow DOM order).
Alignment in Grid
Grid uses the same alignment properties as Flexbox, but applied in two dimensions:
| Property | Axis | Applies to | Controls |
|---|---|---|---|
justify-items |
Inline (horizontal) | All items | How items sit within their cell horizontally |
align-items |
Block (vertical) | All items | How items sit within their cell vertically |
justify-content |
Inline (horizontal) | The grid itself | How the entire grid is positioned in its container |
align-content |
Block (vertical) | The grid itself | How the entire grid is positioned vertically |
justify-self |
Inline (horizontal) | Single item | Override justify-items for one item |
align-self |
Block (vertical) | Single item | Override align-items for one item |
place-items: center - all items centered in their cells
/* Center all items in their cells (both axes) */
.container {
display: grid;
place-items: center; /* shorthand for align-items + justify-items */
}
/* Place shorthands: */
place-items: center; /* align-items: center; justify-items: center; */
place-content: center; /* align-content: center; justify-content: center; */
place-self: end center; /* align-self: end; justify-self: center; */
minmax()
minmax(min, max) sets a track size range. The track will be at least min and at most max:
minmax(100px, 1fr) - columns are at least 100px, grow to fill space equally
/* Track is at least 100px, at most 1fr */
grid-template-columns: repeat(3, minmax(100px, 1fr));
/* Row is at least 50px, grows to fit content */
grid-template-rows: minmax(50px, auto);
/* The responsive pattern: */
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
/* Translation: "as many columns as fit, each at least 200px, growing to fill" */
minmax() is what makes auto-fill/auto-fit work. Without it, the browser doesn't know what "fits" means. minmax(200px, 1fr) says "each column needs at least 200px - fit as many as you can, then stretch them equally." This single line replaces most responsive breakpoint logic for card grids.
Subgrid
Subgrid lets a nested grid inherit track definitions from its parent. This solves the classic problem of aligning items across sibling cards:
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.card {
display: grid;
grid-template-rows: subgrid; /* inherit row tracks from parent */
grid-row: span 3; /* card spans 3 parent rows */
}
/* Now all cards' internal rows (title, content, footer) align
across the entire card grid - even if content varies in length */
Without subgrid, each card is an independent grid - its rows are sized by its own content. Card titles might be at different heights if one title wraps. With subgrid, the parent grid's row tracks govern all cards, so titles align.
Browser support: Subgrid is supported in all modern browsers (Chrome 117+, Firefox 71+, Safari 16+). It's the solution to "how do I align content across sibling grid items" - a problem that was previously impossible without fixed heights or JavaScript.
Named lines
You can name grid lines for more readable placement:
.container {
display: grid;
grid-template-columns:
[full-start] 1fr
[content-start] 2fr
[content-end] 1fr
[full-end];
}
/* Place items using names instead of numbers */
.hero { grid-column: full-start / full-end; } /* full width */
.article { grid-column: content-start / content-end; } /* center column only */
/* Named lines + repeat: */
grid-template-columns: repeat(3, [col-start] 1fr [col-end]);
/* Creates: [col-start] 1fr [col-end col-start] 1fr [col-end col-start] 1fr [col-end] */
Named lines vs named areas: Named areas are better for page layouts with distinct regions. Named lines are better for content grids where items need to span specific ranges. You can use both together - grid-template-areas automatically creates named lines like header-start and header-end.
Grid vs Flexbox - when to use which
| Scenario | Grid | Flexbox |
|---|---|---|
| Page layout (header, sidebar, main, footer) | ✓ Named areas | Possible but awkward |
| Card grid with equal-height cards | ✓ auto-fit + subgrid | Needs flex-wrap + fixed basis |
| Navigation bar | Overkill | ✓ Perfect fit |
| Centering a single item | ✓ place-items: center | ✓ justify + align center |
| Items of unknown/variable count in a row | Possible with auto-fit | ✓ Natural fit |
| Overlapping items | ✓ Place on same cell | Needs positioning hacks |
| Content determines layout | Structure must be predefined | ✓ Flexbox adapts to content |
The real answer: Use both. Grid for the macro layout (page structure, component placement). Flexbox for micro layout (items inside a navbar, buttons in a group, icon + text alignment). They compose perfectly - a grid item can be a flex container and vice versa.
Practical patterns
Responsive card grid (no media queries)
Cards auto-wrap with minmax - resize the browser to see it adapt
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
/* That's it. No media queries. Cards wrap responsively.
- Wide screen: 3-4 columns
- Medium: 2 columns
- Narrow: 1 column
All from one line of CSS. */
Holy grail layout
.page {
display: grid;
grid-template-columns: 250px 1fr 200px;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header header"
"nav main aside"
"footer footer footer";
min-height: 100vh;
gap: 1rem;
}
@media (max-width: 768px) {
.page {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"nav"
"aside"
"footer";
}
}
Bento grid
Bento-style layout - items span different numbers of cells
.bento {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 100px;
gap: 1rem;
}
.feature { grid-column: span 2; grid-row: span 2; }
.tall { grid-row: span 2; }
.wide { grid-column: span 2; }
Centering anything (the shortest way)
The simplest centering technique in CSS
.container {
display: grid;
place-items: center;
/* That's it. One line centers the child both ways. */
}
Grid centering vs Flexbox centering: Both work. Grid's place-items: center is slightly shorter than Flexbox's justify-content: center; align-items: center;. Either is fine - use whichever you're already using for the layout.
Retrieval check
Question 1
What does the fr unit represent?
Question 2
What is the difference between auto-fill and auto-fit?
Question 3
How do you make a grid item span from the first column line to the last?
Question 4
What problem does subgrid solve?
Question 5
Which single CSS declaration creates a responsive card grid that adapts without media queries?