Subgrid and intrinsic grid layouts

Align nested content to a parent grid with subgrid, and size tracks from their content with min-content, max-content and fit-content().

  • Beginner
  • 8 min read
  • Updated 17 Oct 2026

What you will learn

  • Align card contents with subgrid
  • Explain how subgrid inherits tracks and gaps
  • Size tracks to content
  • Combine intrinsic sizing with fr and minmax()

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

HTML
<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>
CSS
.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:

CSS
.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:

  1. The card is a grid item of .cards and is placed across three rows with grid-row: span 3. The number must match the number of children you want to align.
  2. grid-template-rows: subgrid means "my rows are the tracks I span in my parent".
  3. 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.75rem above 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:

CSS
.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:

HTML
<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>
CSS
.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:

CSS
.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:

KeywordTrack size
min-contentThe smallest size without overflowing: usually the longest word or widest image
max-contentThe size the content wants with no wrapping at all
autoBetween the two; as a maximum it behaves like max-content, but auto tracks also stretch to fill spare space
CSS
.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:

CSS
.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:

CSS
.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) inside minmax() 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

  • subgrid lets 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 set grid-template-rows: subgrid.
  • Subgrids inherit tracks, gaps and line names; unsupported browsers fall back to a normal grid.
  • min-content, max-content and auto size tracks from content; fit-content() caps that size.
  • Use max-content only for short content, and minmax(0, 1fr) for truly equal columns.

Further reading