Updated Jul 6, 2026

CSS Grid: Two-Dimensional Layout

Flexbox handles one direction at a time. Grid handles rows and columns together, as a single layout - which is what you actually want for a page skeleton: a header across the top, a sidebar down one side, a footer across the bottom, all defined at once instead of nested flex containers fighting each other.

With Flexbox, getting a sidebar to line up with a header above it and a footer below it means carefully matching widths across three separate flex containers that don't know about each other. Grid defines all three regions as one layout, so they can't drift out of alignment - they're cells in the same table-like structure, not three unrelated rows guessing at the same width.

Turning it on

.dashboard {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-rows: auto 1fr auto;
}

Every direct child becomes a grid item, placed into the cells this creates - by default, in source order, left to right, top to bottom.

Defining columns and rows

grid-template-columns and grid-template-rows take a list of sizes, one per track:

.layout {
  display: grid;
  grid-template-columns: 200px 1fr 100px; /* three columns */
  grid-template-rows: 80px auto;          /* two rows */
}

The unit to know is fr (fraction) - it divides leftover space after fixed-size tracks are subtracted, similar in spirit to flex-grow. 1fr 1fr 1fr is three equal columns; 200px 1fr is a fixed sidebar plus a column that eats everything else.

repeat() avoids repeating yourself:

grid-template-columns: repeat(3, 1fr); /* same as 1fr 1fr 1fr */

gap puts consistent space between tracks, no margin hacks on edge items:

.layout {
  display: grid;
  gap: 1rem;
}

Naming the layout: grid-template-areas

The most readable way to build a page skeleton is naming regions and drawing the layout as ASCII art directly in your CSS:

.dashboard {
  display: grid;
  grid-template-columns: 220px 1fr;
  grid-template-rows: 70px 1fr 50px;
  grid-template-areas:
    "header  header"
    "sidebar main"
    "footer  footer";
  gap: 1rem;
  min-height: 100vh;
}

.header  { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main    { grid-area: main; }
.footer  { grid-area: footer; }
<div class="dashboard">
  <header class="header">Dashboard</header>
  <nav class="sidebar">Nav</nav>
  <main class="main">Content</main>
  <footer class="footer">Footer</footer>
</div>

What just happened: grid-template-areas is a literal picture of the layout - "header" spans both columns on row one, "sidebar" and "main" split row two, "footer" spans both columns on row three. Each child is placed by name with grid-area, not by counting rows and columns. Read the CSS, see the page.

💡 Key point. This is the single biggest readability win Grid has over Flexbox. Six months from now, grid-template-areas still reads like a floor plan. Nested flex containers reconstructing the same layout do not.

Spanning cells: grid-column and grid-row

An item can span multiple tracks with grid-column / grid-row, using span:

.featured-photo {
  grid-column: span 2; /* takes up two columns instead of one */
  grid-row: span 2;    /* and two rows */
}

This is how a "featured" item in a grid of photos or cards gets to be visibly bigger than its neighbors while everything else still auto-places around it. You don't have to hand-place every other item either - only the spanning one needs an explicit rule. Grid's auto-placement algorithm flows the rest into whatever cells are left, in source order, exactly like it would without any spanning at all.

Build it: a responsive photo gallery

The pattern for "as many equal-width columns as fit, wrapping automatically, no media queries":

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1rem;
}
<div class="gallery">
  <img src="a.jpg" alt="" />
  <img src="b.jpg" alt="" />
  <img src="c.jpg" alt="" />
  <!-- more images -->
</div>

What just happened: minmax(180px, 1fr) tells each column "never shrink below 180px, but grow to fill available space." auto-fit computes how many 180px+ columns fit the container width and generates exactly that many tracks - four on a wide screen, two on a tablet, one on a phone - with zero @media rules. Resize the browser and watch columns appear and disappear on their own.

⚠️ Gotcha - auto-fit vs auto-fill. auto-fit collapses empty tracks to 0px and lets existing items stretch to fill the row, which is what you want for a gallery. auto-fill keeps empty tracks at their minimum size (visible gaps, items don't stretch) - useful only when you specifically want placeholder-style blank columns.

Lock in the two properties before moving on:

[
  {
    "q": "What does grid-template-areas let you do that raw grid-template-columns/rows doesn't as directly?",
    "choices": ["Add animations to grid items", "Draw the layout as named regions that read like a floor plan", "Make the grid responsive automatically", "Skip using display: grid entirely"],
    "answer": 1,
    "explain": "grid-template-areas names each region and lays it out as ASCII art, so the CSS visually matches the page structure."
  },
  {
    "q": "grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)) produces what kind of layout?",
    "choices": ["A fixed 3-column grid regardless of screen size", "As many 180px-minimum columns as fit the container, wrapping automatically", "A single column on all screen sizes", "A grid that requires JavaScript to resize"],
    "answer": 1,
    "explain": "auto-fit calculates how many minmax-sized tracks fit the available width and generates that many columns, with 1fr letting them stretch to fill the row."
  },
  {
    "q": "What does grid-column: span 2 do to a grid item?",
    "choices": ["Moves it to column 2", "Makes it occupy two columns instead of one", "Duplicates it into two items", "Hides it on screens narrower than 2 columns"],
    "answer": 1,
    "explain": "span 2 tells the item to stretch across two column tracks, letting it sit visibly larger than single-column siblings."
  }
]

← Phase 1: Flexbox: One-Dimensional Layout · Guide overview · Phase 3: Choosing Between Them (and Combining Them) →

Check your understanding 3 questions

1. What does grid-template-areas let you do that raw grid-template-columns/rows doesn't as directly?

2. grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)) produces what kind of layout?

3. What does grid-column: span 2 do to a grid item?