CSS Grid: Two-Dimensional Layout

Published Updated

CSS grid arranges a container's direct children against shared row and column tracks. It suits layouts where items need to line up in two dimensions, including page shells, dashboards, card galleries, and forms with repeated label columns. For the broader styling basics, start with the CSS learning guide. This guide is part of our CSS layout guide series.

Picture a sheet of graph paper placed inside the container. Grid properties draw the track lines, gap opens space between them, and placement rules tell an item which lines it should span. The browser can add more paper when content needs tracks you did not define.

What Does CSS Grid Control?

CSS grid controls rows and columns together. A grid container defines tracks, and its direct children become grid items. The browser places items automatically unless you set their grid positions yourself.

The explicit grid contains tracks declared by grid-template-columns and grid-template-rows. The implicit grid contains tracks the browser creates when extra content or out-of-range placement needs more room. That distinction matters when an unexpected row or column appears.

Track sizes can use fixed lengths, content-based keywords, percentages, or the fr unit. An fr value receives a share of available space after fixed tracks and gaps have been accounted for, which makes it useful for coordinated columns.

Build a Grid Step by Step

The following sequence uses one card container throughout. Each CSS block includes the previous step, so you can replace the earlier rule and observe one layout change at a time.

<section class="card-grid">
  <article class="card feature">Release notes</article>
  <article class="card">Layout guide</article>
  <article class="card">Selector guide</article>
  <article class="card">Color guide</article>
</section>

Turn the Container into a Grid

.card-grid {
  display: grid;
}

The section becomes a grid container, and each article becomes a grid item. With no template columns, auto-placement creates a single column and enough implicit rows to hold the four cards. The result still looks like a vertical stack, but the cards now participate in grid layout.

Define the Column Tracks

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

With this short card content, the graph paper has three columns that divide the available space equally. The first three cards fill the first row, and the fourth card starts an automatically created second row. repeat(3, 1fr) is the shorter form of 1fr 1fr 1fr.

A bare 1fr has an automatic minimum, equivalent to minmax(auto, 1fr), so long content can widen a track or overflow. Use minmax(0, 1fr) when equal columns must not depend on their content.

Add Space Between the Tracks

.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

The cards keep their three-column placement while one rem of space appears between every row and column. The gap belongs to the container, so the outer edges remain free of the doubled or missing margins that often appear when each card manages its own spacing.

Span an Item Across Tracks

.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

.feature {
  grid-column: 1 / -1;
}

The feature card starts at the first explicit column line and ends at the last one, so it spans the full first row. The other three cards move to the next row and occupy one column each. Negative line number -1 means the final explicit grid line, which keeps the span correct if the template later gains another column.

Make the Columns Responsive

.card-grid {
  display: grid;
  grid-template-columns:
    repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  gap: 1rem;
}

.feature {
  grid-column: 1 / -1;
}

auto-fit creates as many tracks as fit, while minmax() lets each track grow from a preferred minimum of 14rem to one share of the available space. The nested min() allows a track to shrink to the container width when the container is narrower than 14rem.

Wide containers can display several columns at once. As the container narrows, cards move to fewer columns without a media query. The feature still spans from the first to final explicit line, so it stays full width throughout the change.

The Second Axis: Rows and Alignment

Everything above sizes columns, which is the half of grid that overlaps with flexbox. The other half is what makes grid two-dimensional: rows you declare, and alignment that works in both directions at once.

.layout {
  display: grid;
  grid-template-rows: auto 1fr auto;
  min-height: 100dvh;
}

One column, three row tracks sized independently: a header that takes the height its content needs, a main region that absorbs whatever is left over, and a footer sized to its own content. That is the sticky-footer problem solved in one declaration, with no calculation and no absolute positioning. Add a grid-template-columns line and the same container becomes a two-dimensional shell, which is the step flexbox cannot take.

Rows you do not declare are created for you, and those implicit rows size to their content unless told otherwise. When a grid receives an unknown number of items, grid-auto-rows is what gives them a consistent height:

.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 12rem;
}

Alignment then works on two levels, and mixing them up is a common source of confusion. justify-items and align-items position each item inside its own cell, with place-items as the shorthand for both. justify-content and align-content position the whole set of tracks inside the container, which only has a visible effect when the tracks do not already fill it.

.tiles {
  display: grid;
  grid-template-columns: repeat(2, 8rem);
  place-items: center;    /* each item, inside its cell */
  place-content: center;  /* the tracks, inside the container */
}

If grid alignment appears to do nothing, check how much space is available. The content-alignment properties need spare space around the tracks. Item alignment needs room inside the cell; an item with an explicit size equal to its cell has nowhere to move.

Naming Areas Instead of Counting Lines

Placing items by line number works, and it becomes hard to read the moment a layout changes. Grid offers an alternative that no other layout system has: draw the layout as text.

.page {
  display: grid;
  grid-template-columns: 16rem 1fr;
  grid-template-areas:
    "sidebar header"
    "sidebar main"
    "sidebar footer";
}

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

Each quoted string is one row and each word is one cell. Repeating a name across cells makes that area span them, which is why sidebar occupies the full height here without any span arithmetic. A single . in place of a name leaves a cell deliberately empty.

The advantage arrives when the layout has to change. To move the sidebar to the right, rewrite the area strings and change grid-template-columns to 1fr 16rem so the sidebar keeps its width. Stacking everything for a narrow screen needs the column template changed as well, since the areas have to describe a single column:

@media (width < 48rem) {
  .page {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "sidebar"
      "main"
      "footer";
  }
}

What does not change in either case is the items themselves. Each one still declares grid-area by name, so nothing has to be rewritten to sit between different line numbers.

Two constraints the syntax enforces are worth knowing before a template silently stops working. Every named area must be rectangular, so an L-shaped region is not expressible. And every row string must declare the same number of cells, because an uneven count makes the whole declaration invalid rather than partially applied.

Common Pitfalls & Debugging

Implicit Tracks Appear Unexpectedly

An item creates a new column outside the template or an extra row has a surprising size. The item was placed beyond the explicit grid, so the browser created an implicit track. Correct the placement line or set grid-auto-rows and grid-auto-columns when implicit tracks are intentional.

Percentage Tracks Overflow with Gap

Two 50% columns plus a gap extend past the container. Each percentage uses the container width, and the gap is added after those tracks. Use repeat(2, minmax(0, 1fr)) so grid subtracts the gap and the tracks can shrink below their content-based minimum.

.two-column {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
}

The two tracks now divide the space left after the one-rem gap. Their zero minimum lets each track shrink within the container when its content is wider.

Grid Is Used for a One-dimensional Row

A toolbar needs several placement rules whenever labels change width. Grid tracks are coordinating a layout whose main problem is distributing items along one row. Move one-dimensional rows to CSS flexbox.

A nested grid normally has its own tracks. Use grid-template-columns: subgrid on a grid item that also has display: grid when its children need to share the parent’s column tracks. The item must span the columns you want it to reuse. Rows can use subgrid in the same way.

Start with the Grid Tracks

Define the track system first, then add spacing and item placement only where the content needs them. Responsive auto-fit tracks let the graph paper redraw itself as the container changes width.

Frequently Asked Questions

How do you place an item in a specific CSS grid cell?

Place a CSS grid item with grid-column and grid-row, or give it a named grid-area defined by the container. These properties change its visual position, so keep the HTML order suitable for reading and keyboard navigation.

What does 1fr mean in CSS grid?

The fr unit represents a fraction of the available space after fixed track sizes, gaps, and automatic track minimums are accounted for. Three 1fr columns receive equal shares of the remaining space, but long content can still make a track wider. Use minmax(0, 1fr) when content must not affect equality.

How do auto-fit and auto-fill differ in CSS grid?

In CSS grid, auto-fill preserves empty repeated tracks when fewer items are available. auto-fit collapses those empty tracks so occupied flexible tracks can expand. Both calculate how many tracks fit in the available space.

Does grid placement change the HTML reading order?

CSS grid placement changes visual position without changing the HTML order. Screen readers and normal keyboard focus order still follow the document, so keep that order meaningful.

Sources

  1. [1]
    Basic concepts of grid layout
    (developer.mozilla.org)
  2. [2]
    grid-template-columns
    (developer.mozilla.org)
  3. [3]
    grid-column
    (developer.mozilla.org)