Skip to content

Grid

CSS Grid lays out children in two dimensions at once. You define a set of rows and columns on the container, and items are placed into the resulting cells — either automatically or by explicit coordinates.

The mental shift from flexbox: with flexbox the content drives the layout; with grid the layout is defined up front and content is placed into it.

.grid {
display: grid;
grid-template-columns: 200px 1fr 200px;
grid-template-rows: auto 1fr auto;
gap: 1rem;
}

That creates a 3×3 grid. Direct children fill the cells in order, left to right then top to bottom.

  • Track — a row or a column.
  • Line — the boundaries between tracks. An n-track axis has n+1 lines, numbered from 1. Negative numbers count from the end, so -1 is always the last line.
  • Cell — the intersection of one row and one column.
  • Area — a rectangle of one or more cells.

fr is a fraction of the leftover space after fixed-size tracks, gaps, and content-sized tracks are accounted for.

grid-template-columns: 200px 1fr 2fr;
/* 200px is taken first; the rest is split 1:2 */

1fr 1fr 1fr gives three equal columns — but only if the content fits. 1fr is shorthand for minmax(auto, 1fr), and that auto minimum means a track will refuse to shrink below its content’s minimum size.

grid-template-columns:
auto /* size to content, but grows to absorb free space if no fr present */
min-content /* narrowest without overflow */
max-content /* widest, no wrapping */
fit-content(300px) /* like max-content, capped at 300px */
minmax(150px, 1fr) /* at least 150px, at most one fraction */
15%;
grid-template-columns: repeat(12, 1fr); /* 12-column grid */
grid-template-columns: repeat(3, 200px);
grid-template-columns: 250px repeat(2, 1fr) 250px; /* mixed */
grid-template-columns: repeat(2, 1fr 2fr); /* -> 1fr 2fr 1fr 2fr */

Both let the browser decide the track count from the available width.

.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1rem;
}
  • auto-fill creates as many tracks as fit, leaving empty tracks if there are not enough items. Columns keep their slot.
  • auto-fit creates the same tracks, then collapses the empty ones to zero, so the remaining items stretch to fill the row.

With 2 items in a container wide enough for 5 columns: auto-fill gives two 240px cards and three empty columns; auto-fit gives two cards that each take half the width.

Use auto-fit when you want items to always fill the row, auto-fill when you want a stable column rhythm.

.page {
display: grid;
grid-template-columns:
[full-start] 1fr
[content-start] minmax(0, 65ch)
[content-end] 1fr [full-end];
}
.page > * { grid-column: content; } /* uses content-start / content-end */
.page > .hero { grid-column: full; } /* breaks out to full width */

A line name of foo with -start/-end suffixes automatically defines an area called foo, which is why grid-column: content works.

The most readable way to define a page skeleton:

.layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
min-height: 100dvh;
gap: 1rem;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }

Rules: every row string must have the same number of cells, an area must form a rectangle, and a . (period) marks an empty cell.

grid-template-areas:
"header header header"
"sidebar main ."
"footer footer footer";

Rearranging for mobile becomes a one-property change:

@media (max-width: 48rem) {
.layout {
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
grid-template-columns: 1fr;
}
}
.item {
grid-column-start: 1;
grid-column-end: 3; /* spans columns 1 and 2 — the end line is exclusive */
grid-row-start: 2;
grid-row-end: 4;
}

Shorthands:

.item { grid-column: 1 / 3; grid-row: 2 / 4; }
.item { grid-area: 2 / 1 / 4 / 3; } /* row-start / col-start / row-end / col-end */

grid-area also takes a name, which is the common form:

.item { grid-area: main; }
.wide { grid-column: span 2; } /* 2 tracks, auto-placed */
.wide { grid-column: 2 / span 3; } /* start at line 2, cover 3 tracks */
.full { grid-column: 1 / -1; } /* first line to last line: full width */

grid-column: 1 / -1 is the workhorse for “this row spans the entire grid”.

Grid has two levels of alignment: the tracks inside the container, and the items inside their cells.

.grid {
justify-items: stretch; /* inline axis: start | end | center | stretch (default) */
align-items: stretch; /* block axis */
place-items: center; /* align-items justify-items */
}
.item {
justify-self: end; /* override for one item */
align-self: center;
place-self: center end;
}

stretch is the default, which is why grid items fill their cell.

Relevant only when the tracks are smaller than the container (fixed-size tracks, no fr):

.grid {
justify-content: center; /* inline axis: start|end|center|space-between|around|evenly */
align-content: center; /* block axis */
place-content: center;
}

Centring anything, the shortest version:

.center { display: grid; place-items: center; min-height: 100dvh; }

Items that do not fit into the explicit tracks create new ones automatically.

.grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* explicit columns */
grid-auto-rows: minmax(120px, auto); /* implicit rows get this size */
gap: 1rem;
}

With 10 items and 3 explicit columns, grid creates 4 implicit rows sized by grid-auto-rows. Without grid-auto-rows, implicit tracks are auto (content-sized).

.grid { grid-auto-flow: row; } /* default: fill rows, add new rows */
.grid { grid-auto-flow: column; } /* fill columns, add new columns */
.grid { grid-auto-flow: row dense; } /* backfill holes left by spanning items */

dense packs items into earlier gaps. It changes the visual order relative to the DOM, so the same accessibility caveat as flexbox order applies: keyboard and screen-reader order still follow the DOM.

grid-auto-columns sizes implicit columns, and is what you use for a horizontal auto-flow strip:

.strip {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(220px, 1fr);
gap: 1rem;
overflow-x: auto;
}

The single most useful grid pattern:

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

The min(260px, 100%) guard matters: on a viewport narrower than 260px, plain minmax(260px, 1fr) forces a track wider than the container and the page scrolls sideways. min(260px, 100%) caps the minimum at the container width.

A “sidebar that becomes a stack” with no breakpoint:

.with-sidebar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
gap: 2rem;
}

Below 2 × 20rem + gap, auto-fit drops to one column and the layout stacks.

A holy-grail app shell:

.app {
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-columns: minmax(0, 16rem) minmax(0, 1fr);
grid-template-areas:
"header header"
"nav main"
"footer footer";
min-height: 100dvh;
}

Grid items can occupy the same cell, and paint order follows DOM order (or z-index). This makes overlays trivial without absolute positioning:

.hero {
display: grid;
grid-template-areas: "stack";
}
.hero > * { grid-area: stack; } /* everything in the same cell */
.hero img { inline-size: 100%; block-size: 100%; object-fit: cover; }
.hero .caption { place-self: end start; padding: 1rem; color: white; }

Unlike absolute positioning, the image still contributes to the grid’s size, so the container grows with its content.

A grid item that is itself a grid can inherit its parent’s tracks, so nested content aligns across siblings.

.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid; /* use the parent's rows
so titles, bodies and footers line up */
}

Supported in Firefox 71+, Safari 16+, and Chrome 117+. It is the correct solution to “make the buttons in these cards line up regardless of title length”.

Flexbox Grid
Dimensions one axis at a time two axes together
Driven by content size the track definition
Item count unknown / variable is fine works either way
Wrapping flex-wrap, lines are independent tracks stay aligned across rows
Best for navbars, toolbars, button rows, centring, “push this to the end” page skeletons, card galleries, forms, dashboards, anything that must align in both directions

They compose. A grid cell is often a flex container, and a flex item is often a grid.

Rule of thumb: if you find yourself setting widths on flex items to make columns line up between rows, you want grid. If you find yourself defining a single row of unknown items, you want flex.

  • 1fr is minmax(auto, 1fr); use minmax(0, 1fr) whenever content might overflow.
  • auto-fill keeps empty tracks; auto-fit collapses them so items stretch.
  • repeat(auto-fit, minmax(min(260px, 100%), 1fr)) is a complete responsive grid with no media queries.
  • The end line is exclusive: grid-column: 1 / 3 covers two tracks. 1 / -1 spans the explicit grid.
  • grid-template-areas gives readable layouts and makes responsive rearrangement one property.
  • place-items: center on a grid container centres everything, both axes, one line.
  • Items sharing a cell overlap in DOM order — a cleaner overlay pattern than absolute positioning.