CSS Grid Layout Tutorial

 

CSS Grid Layout Tutorial

There was a long stretch of web development history where building a real, proper grid layout meant reaching for a framework. Bootstrap's 12-column system, countless float-based grid hacks, table layouts that everyone agreed were a bad idea but used anyway — for years, CSS simply didn't have a native way to handle true two-dimensional layouts. You could fake it, but it always involved compromises.

CSS Grid changed that completely. It's a layout system built from the ground up for exactly this problem: arranging content into rows and columns simultaneously, with precise control over sizing, spacing, and placement. No frameworks, no hacks, no extra markup just to make the layout work. Just CSS doing what it was always supposed to do.

This tutorial walks through Grid from scratch, building up from the absolute basics to the patterns you'll actually use in real projects. By the end, you'll be comfortable enough to build complex page layouts without reaching for a single grid framework.

What Makes Grid Different from Flexbox

If you've worked with Flexbox before, it's worth addressing this comparison right away, because the two get confused constantly.

Flexbox is one-dimensional. It arranges items along a single line — a row or a column — and excels at distributing space and handling content of unpredictable size. Grid is two-dimensional. It handles rows and columns at the same time, which makes it the right tool whenever you're thinking about a layout as an actual grid: a dashboard, a photo gallery, a full page structure with a header, sidebar, content area, and footer.

A good rule of thumb: if you're laying out items in just one direction, Flexbox is usually simpler. If you're laying out a true grid with both rows and columns that need to line up, Grid is built for exactly that. In real projects, the two often work side by side — Grid for the page skeleton, Flexbox for arranging things within individual sections.

Creating Your First Grid

Like Flexbox, everything starts with a single property on a container element:

.container {
  display: grid;
}

That alone doesn't do much visually yet — you've told the browser "this is a grid container," but you haven't defined any actual rows or columns. Let's fix that.

<div class="container">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
  <div class="item">4</div>
  <div class="item">5</div>
  <div class="item">6</div>
</div>
.container {
  display: grid;
  grid-template-columns: 200px 200px 200px;
  grid-template-rows: 100px 100px;
}

This creates a grid with three columns, each 200px wide, and two rows, each 100px tall. The six items will automatically flow into that grid, filling it left to right, top to bottom, two rows of three. Notice we didn't have to tell each individual item where to go — Grid handled the placement for us automatically, based on the structure we defined.

The fr Unit: Grid's Best Friend

Hardcoding pixel widths gets old fast, especially for layouts that need to adapt to different screen sizes. This is where the fr unit comes in — short for "fraction" — and it's one of the things that makes Grid genuinely pleasant to work with.

.container {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}

This creates three columns that each take up an equal fraction of the available space, automatically adjusting as the container resizes. Want one column twice as wide as the others?

.container {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr;
}

Now the first column claims twice as much space as either of the other two. You can also mix fr units with fixed sizes:

.container {
  display: grid;
  grid-template-columns: 250px 1fr;
}

This is an extremely common pattern — a fixed-width sidebar paired with a main content area that fills whatever space remains. The sidebar always stays exactly 250px, and the main area flexes to fill the rest, no media queries required.

The repeat() Function

Writing out 1fr 1fr 1fr 1fr 1fr 1fr for a six-column grid gets tedious and error-prone. The repeat() function exists specifically to clean this up:

.container {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
}

This produces the exact same result as typing 1fr six times, just far more readable. You can mix repeated and individual values too:

.container {
  display: grid;
  grid-template-columns: 200px repeat(3, 1fr) 100px;
}

That gives you a fixed 200px first column, three equal flexible columns in the middle, and a fixed 100px last column.

gap: Spacing Without the Hassle

Just like in Flexbox, gap handles spacing between grid items cleanly, without needing margin hacks on individual items.

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

You can control row and column spacing independently if needed:

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  row-gap: 30px;
  column-gap: 15px;
}

The spacing only appears between items — there's no extra space added around the outer edge of the grid itself, which is exactly the behavior you usually want.

Placing Items Manually

So far, items have been flowing into the grid automatically. Sometimes you need more control — placing a specific item in a specific spot, or making it span multiple rows or columns. Grid gives you precise tools for this using line numbers.

Here's the key mental shift: Grid doesn't just think in terms of rows and columns, it thinks in terms of the lines that separate them. A three-column grid has four vertical grid lines — one before the first column, one between each pair of columns, and one after the last column.

.item-1 {
  grid-column: 1 / 3;
}

This tells the browser: start this item at column line 1, and stretch it until column line 3. Since column line 3 sits between the second and third columns, this item ends up spanning the first two columns. There's a more readable shorthand for this exact situation too:

.item-1 {
  grid-column: span 2;
}

span 2 means "take up two columns starting from wherever this item naturally falls," without needing to know the exact line numbers. The same logic applies to rows:

.item-1 {
  grid-row: 1 / 3;
  grid-column: 1 / 3;
}

This item now spans two rows and two columns, effectively creating a larger block within the grid, similar to a featured image or highlighted card.

grid-template-areas: Naming Your Layout

This is, hands down, one of the most genuinely enjoyable features in all of CSS. Instead of thinking in line numbers, grid-template-areas lets you sketch out your layout almost like ASCII art, using named regions.

.container {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "sidebar header"
    "sidebar content"
    "sidebar footer";
}

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

Each quoted string represents one row, and each word within it represents a column in that row. Repeating a name across multiple cells, like sidebar here, tells the browser that area should span across all of them. Then on each individual element, you just assign it to whichever named area it belongs to using grid-area.

What makes this approach so useful is how readable it is. You can glance at the grid-template-areas block and immediately picture the layout, without mentally tracking line numbers. It's also fantastic for responsive design, because you can completely redefine the layout at different breakpoints just by rewriting the area names:

@media (max-width: 600px) {
  .container {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "content"
      "sidebar"
      "footer";
  }
}

On smaller screens, the sidebar drops below the content and everything stacks into a single column — same elements, same grid-area assignments, completely different visual structure.

minmax(): Flexible Sizing with Limits

Sometimes you want a column or row to be flexible, but only within certain bounds. That's exactly what minmax() is for.

.container {
  display: grid;
  grid-template-columns: minmax(200px, 1fr) 3fr;
}

This says: the first column should never shrink below 200px, but beyond that minimum, it can flex up to take its fair share of space. It's especially useful in responsive designs, preventing sidebars or cards from becoming uncomfortably narrow on smaller screens.

auto-fill and auto-fit: Responsive Grids Without Media Queries

This next combination genuinely feels like a magic trick the first time you see it work. Combining repeat(), auto-fill (or auto-fit), and minmax() lets you build a fully responsive grid that automatically adjusts the number of columns based on available space — no media queries needed at all.

.container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
}

Here's what's happening: the browser calculates how many 200px-minimum columns can fit in the available width, creates that many columns, and then lets them flex to fill any remaining space using the 1fr portion. Resize the browser window, and the grid automatically adds or removes columns to fit — perfect for image galleries, product listings, or card-based layouts.

The difference between auto-fill and auto-fit is subtle but matters in specific cases. auto-fill keeps empty column tracks if there isn't enough content to fill the row completely, while auto-fit collapses those empty tracks and lets existing items stretch to fill the space instead. For most use cases, especially when the number of items varies, auto-fit tends to produce the more intuitive result.

Implicit vs. Explicit Grid

Up to this point, we've been defining an explicit grid — manually specifying exact rows and columns with grid-template-columns and grid-template-rows. But what happens if you add more items than your defined grid has room for?

Grid automatically creates additional rows (or columns, depending on the flow direction) to fit the overflow content. This is called the implicit grid, and you can control how those automatically generated tracks are sized:

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 150px;
}

Here, you've only defined columns explicitly. Any rows the browser needs to add automatically, to accommodate extra items, will default to 150px tall instead of just sizing themselves based on content.

grid-auto-flow: Controlling the Fill Direction

By default, Grid fills items row by row, left to right, top to bottom. You can flip this to fill column by column instead:

.container {
  display: grid;
  grid-template-rows: repeat(3, 100px);
  grid-auto-flow: column;
}

There's also a dense keyword, which tells the browser to backfill any gaps left by items of varying sizes, rather than strictly preserving source order:

.container {
  grid-auto-flow: row dense;
}

Use dense carefully — it can fill gaps efficiently, but it also reorders items visually away from their source order, which can create the same accessibility concerns we talked about with Flexbox's order property.

Aligning Content Within the Grid

Grid offers alignment properties similar to Flexbox, but applied across two dimensions instead of one.

justify-items and align-items control how individual items align within their own grid cell:

.container {
  display: grid;
  justify-items: center;
  align-items: center;
}

justify-content and align-content control how the entire grid is positioned within the container, which only matters when the grid's total size is smaller than the container itself:

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  justify-content: center;
}

And just like Flexbox's align-self, Grid gives you justify-self and align-self for overriding alignment on individual items:

.item-special {
  justify-self: end;
  align-self: start;
}

A Few Real-World Layout Examples

Theory only goes so far, so let's build a couple of patterns you'll genuinely use.

The Classic Page Layout

<div class="page">
  <header class="header">Header</header>
  <nav class="nav">Nav</nav>
  <main class="main">Main Content</main>
  <aside class="sidebar">Sidebar</aside>
  <footer class="footer">Footer</footer>
</div>
.page {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header header"
    "nav main sidebar"
    "footer footer footer"
  ;
  min-height: 100vh;
  gap: 16px;
}

.header  { grid-area: header; }
.nav     { grid-area: nav; }
.main    { grid-area: main; }
.sidebar { grid-area: sidebar; }
.footer  { grid-area: footer; }

This is the layout pattern people used to need entire frameworks for, and here it is in about fifteen lines of plain CSS.

A Responsive Photo Gallery

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 12px;
}

.gallery img {
  width: 100%;
  height: 200px;
  object-fit: cover;
  border-radius: 8px;
}

Drop in any number of images, and the gallery automatically arranges them into as many columns as comfortably fit, reflowing gracefully as the screen resizes.

A Dashboard with Mixed-Size Cards

.dashboard {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 150px;
  gap: 16px;
}

.card-large {
  grid-column: span 2;
  grid-row: span 2;
}

.card-wide {
  grid-column: span 2;
}

Most cards take up a single cell, but specific cards can be flagged as larger or wider, creating that asymmetric dashboard look you see in tools like analytics panels or admin interfaces, without any extra markup or JavaScript.

Common Mistakes to Watch For

A handful of issues tend to trip people up consistently when they're getting comfortable with Grid.

Forgetting that grid-template-areas requires every row to have the same number of column names, even if some of them repeat the same area, is a frequent source of confusing errors. The grid simply won't apply if the shape doesn't line up correctly.

Mixing up justify-items/align-items with justify-content/align-content is another common stumble. Remember: the "items" versions control alignment inside each cell, while the "content" versions control the entire grid's position within its container, and only matter when there's leftover space.

People also sometimes forget that grid line numbers start at 1, not 0, which can cause off-by-one placement errors when manually positioning items with grid-column or grid-row.

And one that catches a lot of people off guard: assuming auto-fill and auto-fit behave identically. They look nearly the same until you have fewer items than the grid has room for, at which point the difference between leftover empty tracks and stretched existing items becomes very noticeable.

Browser Support

CSS Grid has had strong support across all major modern browsers for a long time now, so there's essentially no reason to avoid it on modern projects. If you're working with an extremely old codebase that still needs to support ancient browser versions, you might need fallbacks, but for the vast majority of projects being built today, you can use Grid freely without compatibility concerns.

Grid and Flexbox Together

It bears repeating: you don't have to choose one over the other for an entire project. A very common, very practical pattern is using Grid to lay out the broad page structure — header, sidebar, main content, footer — and then using Flexbox inside individual sections to arrange smaller groups of elements, like a row of buttons or a horizontally scrolling list of tags. They complement each other extremely well, and most real-world codebases lean on both.

How to Practice

The fastest way to get comfortable with Grid is to deliberately rebuild layouts you already know well.

Try recreating a page layout from a site you visit often using grid-template-areas — most page structures (header, sidebar, content, footer) translate beautifully into this approach.

Build a responsive image gallery using the auto-fit and minmax() combination, and resize your browser window to watch it reflow in real time without writing a single media query.

Experiment with spanning items across multiple rows and columns to build an asymmetric dashboard layout. Seeing items overlap and span cells firsthand makes the line-based placement system click far faster than reading about it in isolation.

Wrapping Up

CSS Grid finally gave the web a native, genuinely capable two-dimensional layout system, and it shows in how naturally complex page structures come together once you're comfortable with it. Defining your grid with grid-template-columns and grid-template-rows, naming regions with grid-template-areas, and building responsiveness through auto-fit and minmax() covers the vast majority of real layout problems you'll run into.

Spend some time rebuilding a few familiar layouts with it, get comfortable thinking in terms of rows, columns, and named areas, and Grid will quickly become the layout tool you reach for instinctively, the same way Flexbox does for simpler one-dimensional arrangements.

Feature Shopify Custom MERN
Development Time Fast (Hours/Days) Slow (Weeks/Months)
Scalability Moderate High (Custom)