CSS Grid and Subgrid Layouts for Responsive Interfaces
CSS Grid is the only layout system on the web that controls both axes simultaneously, and subgrid extends that power across component boundaries so nested elements can share a parent's track lines. This guide sits under Mastering Container Queries & Responsive Layouts and focuses on building responsive structures with track sizing, grid-template-areas, and subgrid, then pairing them with the element-aware breakpoints you learn in the Container Query Syntax Basics guide. Where Grid decides where boxes go, the intrinsic keywords covered in Intrinsic Sizing Techniques decide how large each track grows, so the two are constantly used together.
Key implementation points:
- Sizing tracks with
fr,minmax(),repeat(), and intrinsic keywords - Naming regions with
grid-template-areasfor self-documenting layouts - Propagating track lines into nested components with
subgrid - Adapting a Grid component's internals with
@container
Prerequisites
This guide assumes you are comfortable with the core layout primitives before adding subgrid on top:
- You can set
display: gridand read the difference between explicit and implicit tracks. - You understand
frunits and how they distribute leftover space after fixed and content-based tracks resolve. - You know what a grid line is (the numbered boundaries between tracks) versus a grid track (the space between two lines).
- You have a working mental model of
container-type: inline-size; if not, start with the container-queries guide linked above.
Core concept: the grid formatting context and track propagation
When you set display: grid, the element becomes a grid container and establishes a grid formatting context. The container computes a set of column lines and row lines from grid-template-columns and grid-template-rows; every direct child is then placed between those lines, either automatically or via explicit placement.
A normal nested grid starts a brand-new, independent formatting context. Its lines bear no relationship to the parent's lines, which is why two sibling cards laid out with independent inner grids never align their internal rows. subgrid changes this. Per the CSS Grid Layout Module Level 2 specification, declaring grid-template-columns: subgrid (or grid-template-rows: subgrid) tells the element to adopt the track sizes and line positions of the area it spans in its parent grid, rather than computing its own. The subgrid still creates a formatting context for its own children, but the lines those children place against are the parent's lines projected into the child.
The practical consequence: items inside separate subgrids that span the same parent tracks line up to the pixel, with no shared wrapper and no magic numbers. This is the mechanism that finally makes "align the buttons across a row of cards" a pure-CSS problem.
Syntax and parameters
| Token | Accepted values | Default |
|---|---|---|
grid-template-columns / grid-template-rows | track list, none, subgrid, repeat() expressions, masonry (experimental) | none |
subgrid | used as the entire value of a template axis; optionally followed by a line-name list in [ ] | — |
minmax(min, max) | any two track sizes; min cannot be an fr unit | — |
repeat(count, tracks) | integer count, or auto-fill / auto-fit for intrinsic repetition | — |
fr | positive <flex> value distributing free space | — |
gap (row-gap / column-gap) | length, percentage, or normal | normal |
grid-template-areas | strings of named cells; . denotes an empty cell | none |
A subgrid only inherits the axis on which you declare it. You can write grid-template-columns: subgrid; grid-template-rows: 1fr auto; to share columns with the parent while defining your own rows. Gaps are inherited from the parent along a subgridded axis by default, but you can override them on the subgrid itself.
Step-by-step implementation
Step 1 — Build a responsive parent grid with intrinsic track sizing
Start with a parent grid that holds a sidebar and a flexible content column. Use minmax() so the sidebar never collapses below a usable width and never bloats past a maximum.
.layout {
display: grid;
/* Sidebar between 14rem and 20rem; content takes the rest. */
grid-template-columns: minmax(14rem, 20rem) 1fr;
gap: 2rem;
}
Step 2 — Name regions with grid-template-areas
For multi-region layouts, named areas read far better than line numbers and make the structure obvious at a glance.
.layout {
display: grid;
grid-template-columns: minmax(14rem, 20rem) 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"sidebar header"
"sidebar main"
"sidebar footer";
gap: 1.5rem;
}
.layout > .sidebar { grid-area: sidebar; }
.layout > .header { grid-area: header; }
.layout > .main { grid-area: main; }
.layout > .footer { grid-area: footer; }
Step 3 — Lay out a repeating set of cards
Use repeat() with auto-fit and minmax() so the column count adapts to available width without any breakpoints.
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1.5rem;
}
Step 4 — Make each card a subgrid so internals align across the row
Give each card three internal rows (media, body, actions) and have the card inherit those rows from a parent that defines them. The cards now share row lines, so every action bar sits on the same baseline regardless of body length.
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
/* Define the three shared row tracks on the parent. */
grid-template-rows: auto 1fr auto;
gap: 1.5rem;
}
.card {
display: grid;
/* Each card spans all three parent rows and adopts their sizes. */
grid-row: span 3;
grid-template-rows: subgrid;
}
Step 5 — Adapt the card's internals with @container
Now make each card responsive to its own width rather than the viewport. Declaring containment lets the inner layout reflow per card, which is exactly what you want in a grid where cards vary in width.
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
container-type: inline-size;
container-name: card;
}
@container card (min-width: 22rem) {
.card__body {
/* Wider cards show media beside text instead of stacked. */
display: grid;
grid-template-columns: 8rem 1fr;
gap: 1rem;
}
}
Annotated production example
A complete, copy-paste pricing-cards section. The outer grid auto-fits columns, the shared row tracks plus subgrid keep titles, descriptions, and call-to-action buttons aligned across all cards, and a @container query upgrades each card's internal layout when it has room.
<section class="pricing">
<article class="plan">
<header class="plan__head"><h3>Starter</h3></header>
<div class="plan__body">
<p class="plan__price">$0</p>
<p class="plan__copy">For trying things out on a single project.</p>
</div>
<footer class="plan__foot"><a class="plan__cta" href="#">Choose</a></footer>
</article>
<article class="plan">
<header class="plan__head"><h3>Team</h3></header>
<div class="plan__body">
<p class="plan__price">$24</p>
<p class="plan__copy">Shared workspaces, roles, and longer history for small teams that ship together.</p>
</div>
<footer class="plan__foot"><a class="plan__cta" href="#">Choose</a></footer>
</article>
<article class="plan">
<header class="plan__head"><h3>Scale</h3></header>
<div class="plan__body">
<p class="plan__price">$99</p>
<p class="plan__copy">SSO and audit logs.</p>
</div>
<footer class="plan__foot"><a class="plan__cta" href="#">Choose</a></footer>
</article>
</section>
<style>
.pricing {
display: grid;
/* Columns grow and shrink without breakpoints. */
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
/* The three shared rows every card will subgrid onto. */
grid-template-rows: auto 1fr auto;
gap: 1.5rem;
padding: 1.5rem;
}
.plan {
display: grid;
grid-row: span 3; /* Occupy all three parent rows. */
grid-template-rows: subgrid; /* Adopt the parent's row sizes/lines. */
gap: 0.75rem;
padding: 1.25rem;
border: 1px solid #d0d7e2;
border-radius: 0.75rem;
container-type: inline-size; /* Query the card, not the viewport. */
container-name: plan;
}
.plan__price { font-size: 2rem; font-weight: 700; margin: 0; }
.plan__copy { margin: 0; color: #5a6472; }
.plan__cta {
display: inline-block;
padding: 0.6rem 1rem;
border-radius: 0.5rem;
background: #2952cc;
color: #fff;
text-decoration: none;
}
/* When a single card is wide enough, give the body two columns. */
@container plan (min-width: 20rem) {
.plan__body {
display: grid;
grid-template-columns: max-content 1fr;
align-items: baseline;
column-gap: 1rem;
}
}
</style>
Because every .plan is a subgrid on rows, the price row, the copy row, and the footer row line up across all three cards even though the middle card has far more copy. Remove grid-template-rows: subgrid and the footers immediately fall out of alignment, which is the single clearest demonstration of what subgrid buys you.
Performance and accessibility notes
Grid layout is resolved on the main thread during layout, so the cost scales with track and item count, not with subgrid specifically. Subgrid is essentially free: it reuses already-computed parent lines rather than running a second independent track-sizing pass. The performance pitfalls people attribute to Grid are almost always container-type: size (which forces block-size containment) used where inline-size would do, so prefer inline-size containment on grid children as covered in the syntax-basics guide.
For accessibility, remember that grid placement is purely visual. grid-template-areas and order-style repositioning do not change DOM order, and screen readers and keyboard tab order follow the DOM. Author the markup in a sensible reading order first, then position with Grid; never use Grid to "fix" a source order that reads wrong. Respect motion preferences when grid changes animate: if you transition track sizes or animate cards reflowing, gate the motion behind prefers-reduced-motion, the same pattern used throughout the Keyframe Animation Patterns guide, where animating layout-affecting grid changes is discussed in depth. Maintain a visible focus indicator on interactive grid items and ensure a 3:1 contrast ratio for any track separators per WCAG 1.4.11.
DevTools debugging workflow
- Turn on the grid overlay. In Chrome or Edge DevTools, open the Elements panel, find the grid container, and click the small
gridbadge next to it. Firefox exposes the same control in the Layout panel under "Grid". - Show line numbers and area names. In the Layout panel, enable "Display line numbers" and "Display area names". This confirms
grid-template-areasmapped where you expect. - Inspect subgrid inheritance. Select the subgrid child and toggle its overlay too. You should see its lines coincide with the parent's lines along the subgridded axis. If they diverge, the child is not actually spanning the tracks you think it is.
- Verify item spans. In the Styles pane, check the computed
grid-row/grid-column. A subgrid that only spans one track inherits only one line pair and will look like a normal grid. - Profile reflow. Use the Performance panel to record a resize. Look for repeated
Layoutevents; if they spike, confirm you are not usingcontainer-type: sizewhereinline-sizesuffices.
Browser compatibility
| Feature | Chrome/Edge | Firefox | Safari | Notes |
|---|---|---|---|---|
CSS Grid (display: grid) | 57+ | 52+ | 10.1+ | Universally available |
subgrid | 117+ | 71+ | 16+ | Cross-browser baseline since late 2023 |
grid-template-areas | 57+ | 52+ | 10.1+ | Universally available |
minmax() / repeat() | 57+ | 52+ | 10.1+ | Universally available |
repeat(auto-fit, …) | 57+ | 52+ | 10.1+ | Universally available |
Subgrid is the only feature here needing a gate for older versions. Wrap subgrid enhancements in @supports (grid-template-columns: subgrid) and provide a non-subgrid baseline (for example, fixed inner row sizes) outside it.
.plan { display: grid; grid-template-rows: 3rem auto 3rem; }
@supports (grid-template-columns: subgrid) {
.plan { grid-row: span 3; grid-template-rows: subgrid; }
}
Common pitfalls
| Pitfall | Cause | Resolution |
|---|---|---|
| Subgrid axis ignores its own track list | Once an axis is subgrid, explicit tracks on that axis are discarded | Remove the conflicting grid-template-* value, or define your own tracks on the other axis only |
| Card internals don't align across the row | The child spans only one row, so it inherits a single line pair | Add grid-row: span N matching the number of shared parent rows |
grid-template-areas shifts reading order | Visual placement diverged from DOM order | Author markup in reading order first; never reorder content-critical regions with Grid |
| Subgrid gaps look wrong | Subgrid inherits parent gaps unless overridden | Set gap explicitly on the subgrid if you need a different inner spacing |
| Layout thrash on resize | container-type: size forces block containment | Use container-type: inline-size on grid children unless block size truly drives layout |
FAQ
What does subgrid actually inherit from its parent grid? A subgrid adopts the parent grid's track sizes and line positions along the axis you declare it on. The child's own items then place onto those inherited lines, so they align with the parent grid and with sibling subgrids.
Can I use subgrid on both rows and columns at once?
Yes. Declare grid-template-rows: subgrid and grid-template-columns: subgrid on the same element to inherit both axes. Each axis is independent, so you can subgrid one axis and define your own tracks on the other.
Do I still need container queries if I use Grid and subgrid? They solve different problems. Grid and subgrid handle two-dimensional placement and cross-component alignment, while container queries change a component's internal layout based on its own width. They compose well together.
Why does my subgrid element ignore its grid-template-rows value?
Because once an axis is set to subgrid, any explicit track list on that axis is ignored. Tracks come entirely from the parent. Remove the conflicting declaration or switch that axis off subgrid.
Related
- Container Query Syntax Basics — declare containment so grid children adapt to their own width.
- Intrinsic Sizing Techniques — the keywords that size your grid tracks.
- Building a Dashboard with Subgrid — apply subgrid alignment across a real dashboard.
- Holy-Grail Layout with Grid — the classic page skeleton with named areas.
- Keyframe Animation Patterns — animate grid and layout changes accessibly.
Related articles
More pages in the same section.