← Back to Index

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.

1 2 3 4 1 2 3

Key terminology:

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)

150px
1fr
1fr
150px
1fr
1fr
.container {
  display: grid;
  grid-template-columns: 150px 1fr 1fr;
  grid-template-rows: auto auto;
  gap: 0.5rem;
}

Track sizes can be:

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

1fr
2fr
1fr
.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

1
2
3
4
/* 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)

1
2
3
4
5

auto-fit - same as above but empty tracks collapse (items stretch to fill)

1
2
3
/* 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

A (col 1-3)
B
C (col 2-4)
D
/* 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

Header
Sidebar
Main Content
Aside
Footer
.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

1
2
3
4
5
.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

1
2
3
4
5
6
/* 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

1
2
3
4
5
6
/* 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; */
Ad

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

Min 100px
Grows to 1fr
Flexible
/* 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 1
Card 2
Card 3
Card 4
Card 5
Card 6
.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

Feature
A
B
Tall
Wide
C
D
.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

Centered!
.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?

A fixed fraction of the total container width
A fraction of the remaining free space after fixed tracks are laid out
A percentage converted to pixels at render time
A font-relative unit like em or rem

Question 2

What is the difference between auto-fill and auto-fit?

auto-fill creates flexible tracks; auto-fit creates fixed tracks
auto-fill works only with minmax(); auto-fit works with any value
auto-fill keeps empty tracks; auto-fit collapses empty tracks so items stretch to fill
They are interchangeable - no practical difference

Question 3

How do you make a grid item span from the first column line to the last?

grid-column: full;
grid-column: 1 / -1;
grid-column: span all;
grid-column: 0 / end;

Question 4

What problem does subgrid solve?

It makes nested grids inherit their parent's gap value
It allows grid items to overflow their cells
It lets nested grids inherit parent track definitions, aligning content across sibling items
It enables transitions on grid-template-columns

Question 5

Which single CSS declaration creates a responsive card grid that adapts without media queries?

display: flex; flex-wrap: wrap;
grid-template-columns: auto auto auto;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-auto-flow: dense;