The core idea: structure without extra markup

CSS Grid differs from every other layout mode in one fundamental way: the grid's rows and columns are defined purely in CSS. That means the structural compartments a layout needs don't have to be created by adding more DOM nodes.

Think about how Table layout works. Each row needs a <tr>, and each cell inside a row needs a <td> or <th>. The document tree has to mirror the visual structure:

<table>
  <tbody>
    <!-- First row -->
    <tr>
      <!-- Cells in the first row -->
      <td></td>
      <td></td>
      <td></td>
    </tr>

    <!-- Second row -->
    <tr>
      <!-- Cells in the second row -->
      <td></td>
      <td></td>
      <td></td>
    </tr>
  </tbody>
</table>

CSS Grid removes that requirement. A single container element can be subdivided into however many compartments the design calls for. The grid children get placed into those compartments, and the actual cutting up of the container happens entirely through CSS properties.

How items flow by default

You opt into grid layout with the display property:

.wrapper {
  display: grid;
}

With no other configuration, the grid has one column, and rows are created as needed for however many children exist. This is called an implicit grid — the browser manufactures rows on the fly, placing each child on its own row.

Because the rows are implicit, the grid is dynamic. Add a child and a new row appears; remove one and the row disappears. Each child always ends up with its own row.

There's an important subtlety here: the grid parent itself isn't even being laid out with the Grid algorithm. It's still a block-level element participating in Flow layout, so its height defaults to whatever its content needs. Grid layout only governs how the children are placed inside that parent.

If you give the parent a fixed height, though, that total surface area gets split into equally sized rows. The children still each take a row, but now the space is divided evenly regardless of what's inside them.

Understanding the unit that isn't a unit

To make columns, you use grid-template-columns. The property accepts any valid CSS length or percentage, and with two values you've sliced the container in two:

<style>  .parent {    display: grid;    grid-template-columns: 25% 75%;  }</style><div class="parent">  <div class="child">1</div>  <div class="child">2</div></div>

Often you'll see fr used instead of percentages. It stands for fraction: the values you pass are treated as proportions. Give grid-template-columns: 1fr 3fr, and the space is split into four total units. The first column gets one, the second gets three — so the first column takes one-quarter of the width, the second takes three-quarters.

<style>  .parent {    display: grid;    grid-template-columns: 1fr 3fr;  }</style><div class="parent">  <div class="child">1</div>  <div class="child">2</div></div>

That sounds similar to percentages, but the difference matters in practice. Try resizing a grid container and comparing the two. Percentages are rigid; an fr column is flexible, in the same way a flex-grow item is.

Here's the concrete difference. Say the first column contains an element with an explicit width of 55px. If the column is a percentage-based one and it gets too narrow, that content will overflow and spill out of its column. If the column uses fr, the grid algorithm will stop shrinking it below the minimum content size needed — even if that breaks the proportional ratio you declared.

Put more precisely, the fr unit only distributes extra space. The algorithm computes each column's content size first, then hands out any leftover room based on the fr ratios. In practice this flexibility helps far more than it hurts.

The difference isn't academic — it shows up with the simplest feature you can add to a grid. The gap property puts a fixed amount of spacing between all columns and rows. But the columns themselves are sized against the full grid area. With percentage columns, two columns that total 100% plus a 16px gap have nowhere to shrink, so the content overflows the container. An fr column doesn't have that problem: it first accounts for the gap and keeps some space for it, then distributes what's left.

Explicit rows versus generated ones

What happens if you declare two columns but have more than two children? The algorithm makes room for every child — it simply spawns a second row and places the overflow there. New rows keep appearing whenever the existing structure runs out of cells.

<style>  .parent {    display: grid;    grid-template-columns: 1fr 3fr;  }</style><div class="parent">  <div class="child">1</div>  <div class="child">2</div>  <div class="child">3</div></div>

That behavior is exactly what you want for something like a photo gallery with a variable number of items. But when you're designing a specific page structure — header, sidebar, main content, footer — you'll want explicit control over both dimensions. That's what grid-template-rows is for:

<style>  .parent {    display: grid;    grid-template-columns: 1fr 3fr;    grid-template-rows: 5rem 1fr;  }</style><div class="parent">  <div class="child"></div>  <div class="child"></div>  <div class="child"></div>  <div class="child"></div></div>

Declaring both properties together produces an explicit grid, where the layout is fully predetermined rather than discovered as children are placed. This is the mode you use for full-page layout.

When a long line of values gets tedious

A seven-day calendar layout is straightforward with grid: seven equal columns. But writing each one out gets tiresome:

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

The repeat() function removes the manual repetition. You still say what you mean — seven columns of one fraction each — you just don't have to type the comma-separated list:

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

Each placeholder represents a working code sample you can experiment with directly.

Placing Grid Children Explicitly

By default, grid children flow into the first available cell, much like tiles laid across a floor. But you can override that automatic placement and tell each child exactly which cell (or cells) it should occupy. The interactive demo below lets you drag children into specific positions; keyboard controls are available if you aren’t using a pointer.

.parent {
  display: grid;
  grid-template-columns:
    repeat(4, 1fr);
  grid-template-rows:
    repeat(4, 1fr);
}

.child {
  

}

To place a child, use the grid-row and grid-column properties. A simple integer assigns the child to that track: grid-column: 3 puts the child in the third column. To make a child span multiple tracks, use a slash to separate the start and end values in one declaration:

.child {
  grid-column: 1 / 4;
}

That slash isn’t a fraction — it’s a value separator, allowing you to set both start and end lines in a single property. Without the shorthand, you’d need two declarations:

.child {
  grid-column-start: 1;
  grid-column-end: 4;
}

There's a subtle trap here: the numbers refer to grid lines, not column indexes. A four-column grid has five vertical lines (plus horizontal lines for rows). To span the first three columns, a child must start at line 1 and end at line 4.

Spanning with the span Keyword

Hard-coding start and end lines works when you know the exact layout, but it’s rigid. For dynamic content, you often want children to auto-place while certain featured items stretch across multiple tracks. Rather than positioning every child manually, use the span keyword on the items that need to be wider:

.grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(3, 1fr);
}
.featured.child {
  grid-column: span 2;
}

Try editing the live example below to see how a featured item can occupy two or more columns while other children flow around it:

<style>  .grid {    display: grid;    grid-template-columns: repeat(4, 1fr);    grid-template-rows: repeat(3, 1fr);  }  .featured.child {    grid-column: span 2;  }</style><div class="grid">  <div class="child"></div>  <div class="child"></div>  <div class="featured child"></div>  <div class="child"></div>  <div class="featured child"></div>  <div class="child"></div>  <div class="child"></div></div>

Defining Layouts with Grid Areas

For explicit layouts, named grid areas are often clearer than numeric lines. Consider a classic page structure with a header, a main content area, and a sidebar. Instead of assigning each child with grid-row and grid-column, you can define the whole layout in one place:

.grid {
  display: grid;
  grid-template-columns: 2fr 5fr;
  grid-template-rows: 50px 1fr;
}

.sidebar {
  grid-column: 1;
  grid-row: 1 / 3;
}
.header {
  grid-column: 2;
  grid-row: 1;
}
.main {
  grid-column: 2;
  grid-row: 2;
}

The magic is in a single declaration that acts like an ASCII-art diagram of your grid:

.parent {
  grid-template-areas:
    'sidebar header'
    'sidebar main';
}

Each row in the template is a string, and each word names a cell. Repeating a name—like sidebar in both rows above—makes that area span multiple tracks. You then assign children to areas with grid-area. This approach gives semantic meaning to your layout and is ideal for grids with a fixed shape. For implicit grids that auto-generate rows and columns, the numeric grid-column and grid-row properties are often a better fit.

A Note on Keyboard Navigation

Reordering children visually doesn't reorder them for keyboard users. Tab order follows the DOM, not the grid’s visual layout. In the example below, the buttons appear in order on screen, but tabbing will jump according to their source order:

<div class="wrapper">  <button class="btn one">    One  </button>  <button class="btn four">    Four  </button>  <button class="btn six">    Six  </button>  <button class="btn two">    Two  </button>  <button class="btn five">    Five  </button>  <button class="btn three">    Three  </button></div>

This can make focus outlines leap around unpredictably. The fix is to arrange the children in the DOM to match the visual order. This approach also keeps layouts accessible for right-to-left languages, where grid columns mirror automatically and the DOM order still holds up.

Controlling Alignment

Grid tracks don’t have to fill the entire container. If your columns are narrower than the parent, you can control their distribution with justify-content, applying the same alignment properties used in Flexbox to arrange the grid’s compartments:

Arrow illustrating the leftover space

To align the items inside their cells rather than the cells themselves, use justify-items. By default, children stretch to fill their column. Setting a different value—like start or center—makes each child shrink to its content width, which lets items in the same column vary in size. For per-child control, the justify-self property overrides the parent’s justify-items default.

Vertical Alignment

The same logic applies to rows, with a parallel set of properties. A quick reference for the naming scheme:

  • justify — operates on columns.
  • align — operates on rows.
  • content — positions the grid structure itself.
  • items — positions the DOM nodes inside their cells.

So align-content positions the rows, and align-items handles the vertical alignment of items within their grid area. The align-self property gives individual children vertical control, just as justify-self does horizontally.

For a quick centering trick, the place-content shorthand combines both axes. On a single-child grid, two declarations are all you need to center it perfectly:

.parent {
  justify-content: center;
  align-content: center;
}

This works because place-content: center is shorthand for justify-content: center and align-content: center, which pushes the single row and column to the center of the container.

Beyond the Basics

This covers the core mechanics of CSS Grid, but the specification goes much deeper. For a complete, interactive curriculum built for developers who work with JavaScript frameworks, the author has created CSS for JavaScript Developers. It offers the same interactive style, plus videos and projects for hands-on practice with the full CSS language.