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 tracksgrid-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.
- Moved column definition to a single parent
.comparisongrid withgrid-template-columns: 64px minmax(0, 2fr) minmax(0, 1.5fr) 5.5rem 7.5rem(fixed width on price and button columns for tabular numerals). - Replaced per-row templates with
grid-template-columns: subgrid; grid-column: 1 / -1;. - Applied
font-variant-numeric: tabular-numson the price cell so digits align within the inherited track. - 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.
- 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 repeat72px 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-gapon 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-columnsonce on the parent. - Set
grid-template-columns: subgridandgrid-column: 1 / -1on each row. - Use fixed or
minmax()tracks for numeric columns (price, counts). - Apply
tabular-numson aligned number columns. - Wrap subgrid rules in
@supportswith 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: subgridwithgrid-column: 1 / -1for 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
@supportskeeps older browsers usable while modern engines get pixel-perfect alignment.
Related reading
- CSS Flexbox and Grid layout explained — one- and two-dimensional layout primitives subgrid extends
- CSS container queries explained — component-responsive layout when shared column edges are not required
- Responsive web design explained — breakpoints and fluid layout alongside grid systems
- Core Web Vitals explained — CLS and layout stability benefits of declarative alignment