Overview

Grid is the layout primitive for two-dimensional work: rows and columns that line up. Use it for page shells, dashboards, card galleries, and form rows where labels and inputs align across multiple rows. The umbrella overview lives in css; this page is the depth for the cases where grid earns its keep.

Use grid for 2D, flex for 1D

Pick by the dimension count, not by familiarity. A single row or column with items that grow and shrink is flex territory; see css-flexbox. Anything with both axes locked, or alignment that must survive across rows, is grid.

.page {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: auto 1fr auto;
  min-block-size: 100dvh;
}

Nesting flex inside grid inside flex is normal. Mix freely; do not force one primitive to do both jobs.

Size tracks with fr, minmax(), and auto-fill

Avoid pixel-perfect grids. Use intrinsic sizing so the layout absorbs content variance.

  • 1fr distributes free space after intrinsic sizes resolve.
  • minmax(min, max) clamps a track so it never collapses or runs away.
  • repeat(auto-fill, minmax(16rem, 1fr)) makes a responsive card grid in one line.
.cards {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
}

Use auto-fit instead of auto-fill only when you want surviving items to stretch and fill the row. The two read the same and behave differently at the last row.

Reach for subgrid when children must align with the parent

Subgrid lets a nested grid inherit the parent’s tracks. Use it when card internals (title, body, footer) need to line up across cards regardless of content height.

.cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto 1fr auto;
}
.card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
}

Subgrid ships in every evergreen browser as of 2025. Use it instead of brittle row-height hacks or JavaScript that measures and rewrites styles.

Name lines and areas; do not count columns

Grid lines numbered 1, 2, 3 are unreadable two months later. Name the lines, or name the areas.

.page {
  display: grid;
  grid-template-columns: [sidebar] 240px [content-start] 1fr [content-end];
  grid-template-areas:
    "sidebar header"
    "sidebar main"
    "sidebar footer";
}
.header {
  grid-area: header;
}
.main {
  grid-area: main;
}

Named areas survive refactors. A new column inserted in the middle reorders by name, not by index, and the call sites do not change.

Separate layout from content

The grid container owns the layout. Children own their content. Do not put width, margin-left: auto, or flex on grid items that already participate in tracks.

/* Wrong: fighting the grid with content rules. */
.card {
  width: 320px;
  margin-left: auto;
}
 
/* Right: let the grid size the track. */
.cards {
  grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}

This separation is what makes a grid auditable. The container shows the layout; the items show the content. Pair it with css-custom-properties tokens for spacing and the grid stays editable in one place.

Use gap, not margins

Grid (and flex) have first-class gutters. Use gap, row-gap, and column-gap. Margins on children leak through nested layouts and break with gap-aware utilities. See tailwind for utility-class equivalents (gap-4).

Common pitfalls

  • Items overflowing tracks. A child with min-width: auto (the default) refuses to shrink below its content. Set min-width: 0 on grid items that should compress.
  • Counting numeric lines. Lines 1, 2, 3 break the moment you add a column. Name lines or areas.
  • Mixing fixed and fr tracks without minmax(). A 200px 1fr grid collapses the 1fr track when content overflows. Use minmax(0, 1fr).
  • Hiding tabbed elements with display: none for layout reasons. Hidden grid items skip the tab order; see accessibility for the keyboard rule.
  • Reordering items with order or grid-row-start. Visual order and DOM order desync; screen readers follow the DOM.