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.

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 CSS Grid Controls

A grid container owns the track system. Its direct children become grid items and are placed into available cells by the auto-placement algorithm unless you give them explicit positions. Descendants inside those items keep their own layout unless they also become grid or flex containers.

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 */
}

When an alignment property appears to do nothing, that distinction is usually why. align-content has nothing to distribute if the tracks already fill the container, and align-items has nothing to move if every item is already stretching to fill its cell.

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. Moving the sidebar to the right is a rewrite of the strings alone. 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

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

Percentage Tracks Overflow with Gap

Symptom: two 50% columns plus a gap extend past the container. Cause: each percentage uses the container width, and the gap is added after those tracks. Fix: 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

Symptom: a toolbar needs several placement rules whenever labels change width. Cause: grid tracks are coordinating a layout whose main problem is distributing items along one row. Fix: move one-dimensional rows to CSS flexbox.

Conclusion

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 grid cell?

Give the item grid-column and grid-row values naming the lines it should span, or place it into a named area declared with grid-template-areas. Either way the item lands where you say, whatever its position in the markup.

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.

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

Both keywords repeat tracks according to the available space. auto-fill can preserve empty tracks when there are fewer items than available columns. auto-fit collapses those empty tracks, allowing the occupied tracks to expand.

Does grid placement change the HTML reading order?

No. Grid placement changes an item's visual position without changing its place in the HTML. By default, screen readers and keyboard focus follow the document order, so keep source order meaningful and avoid visual arrangements that contradict it.

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)