A grid only controls its direct children. Elements nested deeper, such as the heading and button inside a card, cannot line up with the same parts of neighbouring cards. subgrid solves that by letting a nested grid use its parent's tracks. This lesson also covers intrinsic sizing: tracks that size themselves from their content.
The problem subgrid solves
<ul class="cards">
<li class="card">
<h2>Sourdough</h2>
<p>Slow-fermented for 24 hours with a crisp, blistered crust.</p>
<a href="/order/sourdough">Order</a>
</li>
<li class="card">
<h2>Seeded rye with sunflower and pumpkin seeds</h2>
<p>Dense and nutty.</p>
<a href="/order/rye">Order</a>
</li>
<li class="card">
<h2>Focaccia</h2>
<p>Olive oil, rosemary and sea salt.</p>
<a href="/order/focaccia">Order</a>
</li>
</ul>
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
gap: 1.5rem;
padding: 0;
list-style: none;
}
.card {
display: grid;
gap: 0.75rem;
padding: 1.5rem;
border: 1px solid #d9e2ec;
}
The cards are the same height, because grid items stretch to fill their row. But inside each card, the long heading in the second card pushes its paragraph and button down. The descriptions and buttons no longer line up across the row. Each card is its own grid, with no knowledge of its neighbours.
Subgrid
Make each card span the three rows it needs in the parent, then tell it to use those rows with subgrid:
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
gap: 1.5rem;
padding: 0;
list-style: none;
}
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
gap: 0.75rem;
padding: 1.5rem;
border: 1px solid #d9e2ec;
}
Now the heading, paragraph and link of every card in a row share the same three row tracks of the parent grid. The heading row is as tall as the tallest heading in that row; the paragraph row as tall as the tallest paragraph. Everything lines up.
Three things make it work:
- The card is a grid item of
.cardsand is placed across three rows withgrid-row: span 3. The number must match the number of children you want to align. grid-template-rows: subgridmeans "my rows are the tracks I span in my parent".- The card's children are placed into those shared tracks.
What a subgrid inherits
- Tracks in the subgridded direction. A subgrid has no track sizes of its own in that axis.
- Gaps, by default. You can override them, as the
gap: 0.75remabove does for the space between the heading, paragraph and link. - Line names from the parent. You can add more of your own.
In the other direction, a subgrid is a normal grid and can define its own tracks. You can also subgrid both directions at once:
.nested {
display: grid;
grid-column: span 2;
grid-row: span 2;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
}
Subgrid for forms
Subgrid also aligns labels and inputs across separate groups:
<form class="form">
<div class="group" role="group" aria-labelledby="delivery-heading">
<h3 id="delivery-heading">Delivery</h3>
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name">
<label for="postcode">Postcode</label>
<input id="postcode" name="postcode" autocomplete="postal-code">
</div>
</form>
.form {
display: grid;
grid-template-columns: max-content 1fr;
gap: 0.75rem 1rem;
}
.group {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
}
.group h3 {
grid-column: 1 / -1;
margin: 0;
}
Every label in every group shares the parent's first column, so they all line up, sized to the longest label in the form. A <fieldset> with a <legend> is often the better element for grouped fields, but browsers lay out the legend specially and do not treat it as a grid item, so test carefully if you combine fieldsets with subgrid.
Browser support and fallback
subgrid is supported in all current major browsers, but it arrived later than Grid itself. Check its Baseline status on web.dev. A browser that does not understand subgrid ignores the grid-template-rows: subgrid declaration, so the card falls back to an ordinary grid with its own rows: still a usable layout, just without cross-card alignment. That makes subgrid a natural progressive enhancement.
For an explicit fallback:
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.card a {
margin-top: auto; /* at least the buttons line up */
}
@supports (grid-template-rows: subgrid) {
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
}
.card a {
margin-top: 0;
}
}
Subgrid works only on an element that is itself a grid item, and only across tracks it spans. If a subgrid seems to have no effect, check that its parent is a grid and that grid-row: span N (or grid-column) matches the number of children.
Intrinsic sizing
Extrinsic sizes come from outside the content: 200px, 50%, 1fr. Intrinsic sizes come from the content itself. Grid tracks accept three intrinsic keywords:
| Keyword | Track size |
|---|---|
min-content | The smallest size without overflowing: usually the longest word or widest image |
max-content | The size the content wants with no wrapping at all |
auto | Between the two; as a maximum it behaves like max-content, but auto tracks also stretch to fill spare space |
.toolbar {
display: grid;
grid-template-columns: max-content 1fr max-content;
gap: 1rem;
align-items: center;
}
The first and last columns are exactly as wide as their content (say, a logo and a button). The middle column takes the rest.
max-content never wraps. A long sentence in a max-content column will stretch the grid beyond its container. Use it for short content like labels, icons and buttons, not for paragraphs.
fit-content()
fit-content(limit) sizes a track to its content, up to a maximum:
.layout {
display: grid;
grid-template-columns: fit-content(20rem) 1fr;
gap: 2rem;
}
The sidebar is as wide as its content needs, but never more than 20rem. If the content is short, the main column gets more room. It behaves like minmax(auto, max-content) clamped at the limit.
Combining with minmax()
Intrinsic keywords work inside minmax() too:
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 14rem), 1fr));
gap: 1rem;
}
.table-like {
display: grid;
grid-template-columns: minmax(max-content, 12rem) repeat(3, minmax(0, 1fr));
}
min(100%, 14rem)insideminmax()stops a single column from overflowing very narrow containers. This pattern is common in intrinsic, media-query-free layouts, which the Responsive Web Design course covers in depth.minmax(0, 1fr)lets columns shrink below their content size, keeping them truly equal.
Debugging subgrid and intrinsic sizes
Enable the grid overlay in DevTools on the parent. Chrome and Firefox label subgrids with a subgrid badge in the Elements panel; turn on both overlays to see the shared lines. The Layout pane shows the computed size of every track, which is the fastest way to see what min-content or fit-content() resolved to.
Summary
subgridlets a nested grid use the tracks it spans in its parent, so contents align across siblings.- Span the right number of tracks (
grid-row: span 3) and setgrid-template-rows: subgrid. - Subgrids inherit tracks, gaps and line names; unsupported browsers fall back to a normal grid.
min-content,max-contentandautosize tracks from content;fit-content()caps that size.- Use
max-contentonly for short content, andminmax(0, 1fr)for truly equal columns.