CSS Subgrid

By

CSS subgrid lets nested grid items align to parent tracks. Build aligned cards and forms, understand spans and gaps, and add a practical fallback.

~~~

CSS subgrid lets a nested grid use tracks defined by its parent grid.

This solves an alignment problem that regular nested grids cannot solve on their own.

Imagine three cards. Each card has a title, description, and button. We can make every card a grid, but each card calculates its rows independently.

A long title makes one card’s button move down. The buttons no longer align across the row.

With subgrid, the cards share the parent’s row tracks. Their internal content lines up.

The mental model

A normal nested grid creates a new track system:

.parent {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

.child {
  display: grid;
  grid-template-rows: auto 1fr auto;
}

Every .child has its own three rows. Those rows know nothing about the rows inside sibling children.

A subgrid does not define new track sizes on that axis:

.child {
  display: grid;
  grid-template-rows: subgrid;
}

It uses the parent tracks covered by the child.

That last part is important. The child must be a grid item, and it must span the parent tracks it wants to inherit.

If you need the Grid basics first, read the CSS Grid tutorial or work through the free CSS course.

Build the card markup

Start with three product cards:

<div class="cards">
  <article class="card">
    <h2>Wireless Mouse</h2>
    <p>Ergonomic design with long battery life.</p>
    <button>Add to cart</button>
  </article>

  <article class="card">
    <h2>USB-C Hub with Seven Ports</h2>
    <p>HDMI, USB, Ethernet, and an SD card reader for your laptop.</p>
    <button>Add to cart</button>
  </article>

  <article class="card">
    <h2>Desk Lamp</h2>
    <p>Warm and cool lighting modes.</p>
    <button>Add to cart</button>
  </article>
</div>

Each card has the same three content parts. Their text lengths are different.

See the problem without subgrid

Create the outer columns:

.cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1.5rem;
}

Now make each card an independent grid:

.card {
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 0.75rem;
  padding: 1rem;
  border: 1px solid #d8d8d8;
}

The button sits at the bottom of each card because the middle row uses 1fr.

But titles and descriptions do not share row sizes across cards. A two-line title in one card does not create a two-line title row for its siblings.

The outer cards can have equal heights while the internal content still fails to align.

Share the rows with subgrid

Each card contains three parts, so make it span three parent rows:

.card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
}

The parent creates the needed rows as implicit grid tracks. Because cards in the same card row cover the same three tracks, their title, description, and button rows line up.

Here is the complete card CSS:

.cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1.5rem;
}

.card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
  padding: 1rem;
  border: 1px solid #d8d8d8;
  border-radius: 0.5rem;
}

.card h2,
.card p {
  margin: 0;
}

.card button {
  justify-self: start;
}

The tallest title sets the shared title row. The tallest description sets the shared description row. Every button starts on the same row.

We did not calculate any heights. Content still controls the size.

Why grid-row: span 3 matters

subgrid inherits the tracks covered by the grid item.

Without a row span, each card normally occupies one parent row. Its row subgrid would only have that one track.

This card needs three internal rows:

  1. title
  2. description
  3. button

So it spans three parent rows:

.card {
  grid-row: span 3;
  grid-template-rows: subgrid;
}

If a component has four aligned parts, it can span four tracks instead.

Do not choose the span from visual guesswork. Count the rows the component needs to share.

Subgrid can work on one axis

A grid can use subgrid for rows while defining its own columns:

.card {
  display: grid;
  grid-template-rows: subgrid;
  grid-template-columns: 1fr auto;
}

Or it can inherit columns while sizing its own rows:

.section {
  display: grid;
  grid-template-columns: subgrid;
  grid-template-rows: auto;
}

You can also use subgrid on both axes:

.item {
  display: grid;
  grid-template-columns: subgrid;
  grid-template-rows: subgrid;
}

Choose the axis where alignment must continue through the nested component.

Align form rows with a column subgrid

Subgrid is also useful when repeated fields must share label and control columns.

Use this markup:

<form class="profile-form">
  <div class="field">
    <label for="name">Name</label>
    <input id="name" name="name" />
  </div>

  <div class="field">
    <label for="email">Email address</label>
    <input id="email" name="email" type="email" />
  </div>
</form>

Define the columns once on the form:

.profile-form {
  display: grid;
  grid-template-columns: minmax(8rem, 12rem) minmax(0, 1fr);
  gap: 1rem;
}

Each field spans both columns and inherits them:

.field {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
}

Every label uses the same first column. Every input begins on the same second-column line.

Without subgrid, each .field would need to repeat the column definition. Repeating the same values works until the parent changes or one component uses a slightly different value.

Gaps come from the parent

A subgrid uses the parent’s gap on the inherited axis by default.

.cards {
  row-gap: 1rem;
}

.card {
  grid-template-rows: subgrid;
}

The card receives that row gap between its subgrid tracks.

You can override it on the subgrid:

.card {
  row-gap: 0.5rem;
}

Remember that the subgrid line sits in the middle of the inherited gap. Changing the gap affects how content sits around those shared lines.

Start with the inherited value. Override it only when the component needs different internal spacing.

Padding belongs to the subgrid item

Padding on the card still works:

.card {
  padding: 1rem;
}

The padding creates space between the card border and its subgrid content. It does not create another shared track.

This is useful for cards, but large differences in sibling padding can make aligned content look misaligned. Shared tracks align the grid lines, not the painted edges of differently padded components.

Keep repeated component padding consistent when alignment is the goal.

Named lines continue into the subgrid

Subgrid also carries parent line names into the inherited track system.

.page {
  display: grid;
  grid-template-columns:
    [full-start] 1fr
    [content-start] minmax(0, 45rem)
    [content-end] 1fr
    [full-end];
}

A child spanning the page can use a column subgrid:

.article-section {
  display: grid;
  grid-column: full-start / full-end;
  grid-template-columns: subgrid;
}

Its children can align to the inherited named lines:

.article-section > p {
  grid-column: content-start / content-end;
}

This lets a nested component participate in a page-wide layout without copying track sizes.

Add a fallback

Current browsers support subgrid, but a fallback is still useful when a project targets older engines.

Start with an independent card grid:

.card {
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 0.75rem;
}

Then enhance it:

@supports (grid-template-rows: subgrid) {
  .card {
    grid-row: span 3;
    grid-template-rows: subgrid;
    gap: inherit;
  }
}

The fallback keeps the card readable and places its button at the bottom. Supporting browsers also align content between cards.

Do not make the page depend on subgrid for basic reading order. Use it to improve alignment.

Common mistakes

The first mistake is adding subgrid without making the child a grid:

.card {
  grid-template-rows: subgrid;
}

The declaration has no grid layout to affect. Add display: grid.

The second mistake is forgetting the span. A subgrid only receives the parent tracks it covers.

The third is using subgrid on the wrong axis. grid-template-columns: subgrid cannot align rows.

The fourth is adding an extra wrapper:

<article class="card">
  <div class="card-content">
    <h2>Title</h2>
    <p>Description</p>
    <button>Add to cart</button>
  </div>
</article>

The direct grid item is now .card-content, not the title, paragraph, and button. Either remove the wrapper or make that wrapper participate in the grid too.

The fifth mistake is expecting subgrid to align unrelated grids. The nested grid must inherit tracks from an ancestor grid it belongs to.

When I use subgrid

I use subgrid when a component has two jobs:

Cards, forms, pricing tables, dashboards, and article layouts are good examples.

I use a regular nested grid when the component should size itself independently. A modal or standalone widget usually does not need to inherit page tracks.

Subgrid is not a replacement for Grid. It is the feature that lets a grid continue through one level of component structure.

Tagged: CSS · All topics
~~~

Related posts about css: