CSS Grid: The Two-Dimensional Layout System, Explained Properly

CSS Grid is the only native CSS layout system designed to handle rows and columns simultaneously — Flexbox, by contrast, is fundamentally one-dimensional, even when it wraps. That distinction is why Grid is the right tool for page-level layout (headers, sidebars, footers) and dashboards, while Flexbox stays better suited to distributing items along a single axis, like a toolbar or a nav.

The Vocabulary Grid Tutorials Skip

Grid has its own terms, and half the confusion around it comes from not knowing them:

Named Areas: The Most Readable Way to Build a Layout

grid-template-areas lets you literally draw the layout in CSS as ASCII art, which makes the relationship between markup and visual position obvious at a glance — no counting line numbers.

.page {
  display: grid;
  grid-template-columns: 220px 1fr;
  grid-template-rows: 64px 1fr 56px;
  grid-template-areas:
    "sidebar header"
    "sidebar main"
    "sidebar footer";
  min-height: 100vh;
}
.header  { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main    { grid-area: main; }
.footer  { grid-area: footer; }

Change the layout — say, moving the sidebar to a bottom nav on mobile — by redefining grid-template-areas inside a media query, without touching a single line of HTML.

auto-fit vs. auto-fill: The Difference That Actually Matters

Both go inside repeat() with minmax() and both create a responsive grid without media queries — but they behave differently when there isn't enough content to fill a row, and that difference trips up almost everyone the first time.

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  /* vs: repeat(auto-fill, minmax(200px, 1fr)); */
  gap: 16px;
}
KeywordWith 2 items in a 4-column-wide space
auto-fitCollapses empty tracks — the 2 items stretch to fill the row
auto-fillKeeps empty tracks in place — the 2 items stay their minmax() width, leaving visible gaps

For a card grid where items should stretch to use available space, use auto-fit. For something like a calendar grid where consistent cell width matters more than filling the row, auto-fill is usually correct.

Grid Layouts, Already Built

UIXDraft's 180+ template bundle includes dashboard, portfolio, and landing page layouts built entirely with modern CSS Grid — inspect the source and adapt it directly.

See the templates →

Subgrid: Fixing the Alignment Problem Grid Couldn't Solve Alone

Before subgrid, a card inside a grid couldn't align its internal rows (title, body, footer) with the cards next to it — each card's grid was isolated from its parent. Subgrid lets a nested grid inherit its parent's tracks:

.card-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}
.card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3; /* must span enough rows to inherit from */
}

With subgrid, if one card's title wraps to two lines, every card's body and footer shift down together to stay aligned — something that used to require matching heights with JavaScript.

Filling Gaps in an Uneven Grid with grid-auto-flow: dense

When grid items span different numbers of columns or rows, the default placement algorithm leaves gaps rather than backtrack to fill them — it only ever moves forward. For a masonry-style gallery where visual gaps look broken, dense packing tells the browser to go back and fill holes with later items:

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  grid-auto-flow: dense;
  gap: 12px;
}
.gallery .featured {
  grid-column: span 2;
  grid-row: span 2;
}

Be aware that dense can reorder items visually away from their source order in the DOM — fine for a photo gallery, but a poor choice for anything where reading order matters, like a list of articles, since screen readers still follow DOM order.

Sizing the Implicit Grid

When more items exist than your explicit grid-template-rows accounts for, the browser creates implicit rows using whatever default sizing applies — usually auto, which can produce inconsistent row heights. Control it explicitly:

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: minmax(120px, auto);
  gap: 16px;
}

This guarantees every implicitly-created row is at least 120px tall but can still grow with content — the same minmax() pattern used for columns, just applied to the axis that grows automatically.

Grid vs. Flexbox: A Fast Decision Rule

QuestionAnswer points to
Do rows and columns need to align with each other?Grid
Are you laying out items along a single line that may wrap?Flexbox
Is content driving the size, or is the layout driving content placement?Content-driven → Flexbox. Layout-driven → Grid

In practice, most real interfaces use both: Grid for the page skeleton, Flexbox for aligning items inside individual components like a card's icon-plus-label row.

Placing Items by Line Number for Precise Control

Named areas are the most readable option, but sometimes precise numeric placement is faster — especially for items that need to overlap or break out of the normal flow, like a badge overlapping a card's corner:

.hero-image {
  grid-column: 1 / 3;
  grid-row: 1 / 2;
}
.badge {
  grid-column: 2 / 3;
  grid-row: 1 / 2;
  justify-self: end;
  align-self: start;
  transform: translate(8px, -8px);
}

Negative line numbers count from the end of the grid instead of the start, which is useful when the total track count might change: grid-column: 1 / -1 always spans the full width regardless of how many columns exist.

Mistakes That Show Up Constantly in Grid Code

Frequently Asked Questions

Why isn't gap creating any visible space in my grid?

Usually because the grid items themselves have collapsing margins or a background that visually masks the gap, or because gap was applied to a flex container in a browser old enough to only support it on grid (support has been universal on both since 2021, so this is now rare).

Can grid-template-columns be animated or transitioned?

Track sizes defined in fixed units (px) can transition, but mixed units like 1fr historically couldn't interpolate smoothly. Modern browsers now support animating grid-template-columns and rows directly in most cases — test in your target browsers, and fall back to animating a wrapping element's width if you see jumps.

Does CSS Grid need a fallback for older browsers?

Grid has had full support in all major browsers since 2020, including on mobile. The only realistic exception is if you must support IE11, which never got unprefixed Grid — in that case use @supports (display: grid) to provide a Flexbox fallback. For any project targeting current browsers, this is no longer a practical concern worth designing around.