Guide

CSS subgrid explained

Harbor Commerce's product comparison page listed twelve SKUs in a vertical stack. Each row was a nested CSS Grid with columns for thumbnail, title, specs, price, and action button. Every row defined grid-template-columns: 72px 1fr 1fr auto auto independently — and every row measured 1fr differently. Price digits jumped left and right as titles wrapped; the “Add to cart” buttons formed a jagged staircase. The team tried fixed pixel widths, then JavaScript column sync on resize. CSS subgrid fixed it in twelve lines: each row became display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;, inheriting the parent's five column tracks so every price cell shared the same vertical edge. Subgrid solves a problem plain Grid cannot: when nested components need to align to a parent track list they do not own. This guide covers column and row subgrid, spanning rules, pairing with CSS Grid and container queries, the Harbor comparison-table refactor, a technique decision table, pitfalls, and a production checklist alongside our responsive design guide.

What subgrid does (and what it does not)

In standard CSS Grid, a nested grid is an independent formatting context. Its fr tracks resolve against the nested grid's own width, not the parent's column list. That is correct for self-contained cards but wrong for list rows, form field groups, and comparison tables where visual rhythm depends on shared column boundaries.

Subgrid (Level 2 of the Grid specification) lets a nested grid adopt the parent grid's tracks on one or both axes:

  • grid-template-columns: subgrid; — inherit column tracks
  • grid-template-rows: subgrid; — inherit row tracks
  • Both together for two-dimensional inheritance

The nested grid's children place into the parent's track lines, so a price column in row 3 aligns with row 7 even when row heights differ. Subgrid does not replace Grid — the parent still defines the master track template. It does not solve component-level responsive layout (that is container queries). It does not work in Flexbox; only grid items that are themselves grids can subgrid.

Minimal column subgrid example

Parent defines the canonical columns; each child row spans the full width and subgrids:

.comparison {
  display: grid;
  grid-template-columns: 72px 2fr 1.5fr 96px 120px;
  gap: 0.75rem 1rem;
}

.comparison__row {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;   /* span all parent columns */
  align-items: center;
}

Each .comparison__row child (thumbnail, title, specs, price, button) places into columns 1–5 of the parent grid. When the title in row 2 wraps to three lines, row 2 grows taller but column 4 (price) stays locked to the same vertical line as every other row.

Row subgrid for magazine layouts

Row subgrid is less common but powerful when a parent defines explicit row tracks (e.g. a 12-row editorial template) and nested sections must align baselines across columns:

.magazine {
  display: grid;
  grid-template-rows: repeat(12, minmax(0, auto));
}

.magazine__feature {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: 1 / span 6;
}

Headlines, pull quotes, and captions in the feature column share row lines with adjacent sidebar content. Use row subgrid when horizontal alignment across columns matters as much as vertical alignment down a list.

Spanning, gaps, and named lines

A subgrid item must span the tracks it intends to inherit. For column subgrid, grid-column: 1 / -1 is the usual pattern when the nested grid should cover every parent column. Partial spans are valid: a row that only subgrids columns 2–4 leaves columns 1 and 5 to siblings or empty cells.

Gaps: the parent's gap applies between top-level rows. Subgrid children participate in the parent gap model on the subgridded axis; nested internal gaps are rare and often break alignment. Prefer parent-level row-gap for list spacing.

Named grid lines on the parent propagate to subgrids. If the parent uses grid-template-columns: [thumb] 72px [body] 1fr [price] auto, nested items can target grid-column: body / price for readable placement. Name lines when the same comparison layout appears in multiple templates.

Subgrid and implicit tracks

Subgrid inherits explicit parent tracks on the subgridded axis. If the parent uses auto-fit or auto-fill with minmax(), track counts can change at breakpoints — subgrid rows still follow, but test resize behavior. Avoid mixing subgrid rows with deeply nested grids that add their own implicit columns; alignment surprises usually mean a child is not actually subgridding.

Practical patterns

Product and pricing tables

The Harbor Commerce use case: vertically stacked rows, each row a component with identical column semantics. Subgrid removes per-row grid-template-columns duplication and eliminates ResizeObserver hacks.

Form field grids

Settings pages with label / input / hint / error in four columns benefit when validation messages vary in height. Subgrid keeps labels right-aligned to a shared column edge while inputs stretch in column 2.

Card lists with aligned metadata

A feed of cards where each card has title, excerpt, author avatar, date, and actions: parent grid defines columns; each card subgrids so dates align even when excerpts differ in length. Pair with :has() to highlight rows containing errors without breaking column sync.

Dashboard widgets in a shared column system

A 12-column page grid where individual widgets subgrid only the columns they occupy (grid-column: 3 / 9 plus column subgrid) keeps KPI numbers aligned across widgets in the same band without flattening the entire DOM into one mega-grid.

Worked example: Harbor Commerce comparison table refactor

Before subgrid, Harbor Commerce maintained a shared constant COLUMN_TEMPLATE in JavaScript, applied as inline styles on each React row, and ran ResizeObserver to re-measure when fonts loaded. CLS on the price column averaged 0.08 on mobile.

  1. Moved column definition to a single parent .comparison grid with grid-template-columns: 64px minmax(0, 2fr) minmax(0, 1.5fr) 5.5rem 7.5rem (fixed width on price and button columns for tabular numerals).
  2. Replaced per-row templates with grid-template-columns: subgrid; grid-column: 1 / -1;.
  3. Applied font-variant-numeric: tabular-nums on the price cell so digits align within the inherited track.
  4. Added a container query on the parent wrapper: below 480 px container width, switched parent to a two-column layout and disabled subgrid on rows (stacked card layout) — progressive enhancement, not subgrid at all costs.
  5. Removed 140 lines of column-sync JavaScript and the ResizeObserver.

Result: zero horizontal jitter between rows, CLS on price column dropped below 0.02, and the design system documented one parent template instead of five row variants. Page-level Core Web Vitals improved because layout stopped shifting after paint.

Technique decision table

Goal Approach Why
Rows must share column edges in a list or table Column subgrid on each row Inherits parent tracks; no JS sync
Self-contained card with internal layout only Independent nested grid (no subgrid) Card columns need not align with neighbors
Component adapts to its column width, not siblings Container queries Local responsiveness without shared track list
One-dimensional toolbar or nav Flexbox Simpler when column alignment across rows is irrelevant
True HTML table semantics and screen-reader table navigation <table> with colgroup Subgrid is visual layout; tables carry accessibility affordances
Align baselines across columns in editorial layout Row subgrid Inherits parent row tracks on the vertical axis
Legacy browsers without subgrid support Flat single grid or table fallback Feature query @supports (grid-template-columns: subgrid)

Browser support and progressive enhancement

Subgrid is available in Firefox 71+, Safari 16+, and Chrome 117+ (September 2023). It is baseline in modern evergreen browsers but absent in IE and older mobile WebViews. Use:

@supports (grid-template-columns: subgrid) {
  .comparison__row {
    grid-template-columns: subgrid;
    grid-column: 1 / -1;
  }
}

Provide a fallback layout that is usable without subgrid: stacked fields on narrow viewports, or a single-grid flattening where each row is a direct child of the parent (more DOM, but universal). Test fallback in Safari versions your analytics still show.

Common pitfalls

  • Forgetting grid-column: 1 / -1 — the subgrid item occupies one parent cell by default and subgrids only that cell's tracks.
  • Subgridding on a non-grid child — only grid items can subgrid; wrap with an extra element if the component root is not a grid.
  • Duplicate column templates on parent and child — defeats the purpose; child should use subgrid, not repeat 72px 1fr ....
  • Using subgrid inside Flexbox — invalid; parent must be display: grid.
  • Expecting subgrid to fix semantic tables — for data tables with sortable headers and screen-reader column navigation, use <table> or ARIA grid patterns.
  • Nested gaps breaking alignment — row-level column-gap on subgrid children can offset tracks relative to siblings.
  • Auto-fit parent with uneven row spans — when track count changes at breakpoints, verify every row still spans 1 / -1.

Production checklist

  • Confirm rows share column semantics; if not, use independent nested grids.
  • Define the master grid-template-columns once on the parent.
  • Set grid-template-columns: subgrid and grid-column: 1 / -1 on each row.
  • Use fixed or minmax() tracks for numeric columns (price, counts).
  • Apply tabular-nums on aligned number columns.
  • Wrap subgrid rules in @supports with a tested fallback layout.
  • Pair with container queries for narrow containers that should stack, not subgrid.
  • Name grid lines on complex templates for maintainable placement.
  • Test resize, font load, and long translated strings that wrap unpredictably.
  • Verify accessibility: subgrid layouts still need logical reading order and focus paths.

Key takeaways

  • Subgrid lets nested grids inherit parent column or row tracks so list rows align without per-row templates.
  • Use grid-template-columns: subgrid with grid-column: 1 / -1 for full-width row inheritance.
  • Parent owns the track list; children place into shared lines — ideal for comparison tables, forms, and card metadata columns.
  • Combine with container queries for responsive stack fallbacks; use real tables when semantics require them.
  • Progressive enhancement via @supports keeps older browsers usable while modern engines get pixel-perfect alignment.

Related reading