Articles /

Widely supportedlayout

Subgrid and layout

The CSS feature that finally makes card grids align without fixed heights or JavaScript.

The card alignment problem

You have three cards in a row. Card one has a short title. Card two has a long title that wraps to two lines. Card three is in between. Without subgrid, the only way to make their body text start at the same vertical position is: fixed min-height on the title (brittle), JavaScript to equalize heights (script-dependent), or display: flex; flex-direction: column with margin-top: auto on the footer (workaround).

Subgrid is the structural solution. Cards share the parent's row tracks, so alignment is a property of the grid, not a hack.

How subgrid works

/* Parent: define column + row tracks */
.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* 4 row tracks: eyebrow | title | body | footer */
  grid-template-rows: auto auto 1fr auto;
  gap: 1rem;
}

/* Card: opt into the parent's row tracks */
.card {
  display: grid;
  grid-template-rows: subgrid; /* ← inherit parent's rows */
  grid-row: span 4;             /* span all 4 tracks */

  /* Internal spacing */
  padding: 1rem;
  gap: 0; /* no extra gap — padding handles spacing */
}

/* Card children automatically align to parent tracks */
.card .eyebrow { /* occupies row 1 across all cards */ }
.card h2       { /* occupies row 2 — titles align */  }
.card p        { /* occupies row 3 — 1fr, stretches */ }
.card footer   { /* occupies row 4 — always bottom */  }

The 1fr body row is the key: it stretches to fill remaining space, so all footers land at the same position regardless of how much body text is in each card.

Column subgrid

Subgrid works on columns too — useful for aligning labels and inputs in a form, or columns in a definition list.

/* 2-column definition list with aligned term + definition */
.dl-grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.5rem 1.5rem;
  align-items: baseline;
}

/* Each dt/dd pair participates in the 2-column grid */
/* No subgrid needed here — they're direct children */

/* Subgrid on columns in a card */
.card-columns {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1; /* span all parent columns */
}

Named grid lines with subgrid

/* Parent can use named lines */
.card-grid {
  grid-template-rows:
    [eyebrow-start] auto [eyebrow-end title-start]
    auto [title-end body-start]
    1fr [body-end footer-start]
    auto [footer-end];
}

/* Child references named lines through subgrid */
.card-footer {
  grid-row: footer-start / footer-end;
}

Combining with container queries

Container queries control how many columns the grid has; subgrid controls how children align within those columns. Together they give you a fully adaptive, structurally aligned card grid.

.wrapper {
  container-type: inline-size;
  container-name: cards;
}

.card-grid {
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  gap: 1rem;
}

/* Default: 1 column, no subgrid needed */
@container cards (min-width: 32rem) {
  .card-grid { grid-template-columns: repeat(2, 1fr); }
  .card { display: grid; grid-template-rows: subgrid; grid-row: span 4; }
}
@container cards (min-width: 48rem) {
  .card-grid { grid-template-columns: repeat(3, 1fr); }
}
Browser support: Chrome 117+, Safari 16+, Firefox 71+. Widely supported. Fallback: cards just size to their content — misaligned but readable.