30 CSS Grid Layouts15 / 30

Pure CSSMIT licensed

Subgrid Card

The card-alignment problem, finally solved: titles of different lengths, descriptions of different depths — yet every title baseline, every description block and every button aligns PERFECTLY across cards, because each card inherits the parent's row tracks with grid-template-rows: subgrid. The demo shows misaligned vs aligned side by side.

Published Updated

Live Demo
Try it

The code

<section class="cgl-15" aria-label="Subgrid card alignment demo">
  <div class="cgl-15__grid">
    <article class="cgl-15__card">
      <p class="cgl-15__eyebrow">Starter</p>
      <h4>Ship your first project</h4>
      <p class="cgl-15__desc">Every card spans the same four parent rows.</p>
      <a href="#0" class="cgl-15__btn">Choose Starter</a>
    </article>
    <article class="cgl-15__card cgl-15__card--hot">
      <p class="cgl-15__eyebrow">Studio · most popular</p>
      <h4>A much longer title that wraps onto two full lines</h4>
      <p class="cgl-15__desc">The title track grows for the tallest title — and every sibling's title track grows with it. That is subgrid.</p>
      <a href="#0" class="cgl-15__btn">Choose Studio</a>
    </article>
    <article class="cgl-15__card">
      <p class="cgl-15__eyebrow">Agency</p>
      <h4>Scale it</h4>
      <p class="cgl-15__desc">Short title, short copy — the button still lands on the shared final track, perfectly level with its neighbours.</p>
      <a href="#0" class="cgl-15__btn">Choose Agency</a>
    </article>
  </div>
</section>
.cgl-15,
.cgl-15 *,
.cgl-15 *::before,
.cgl-15 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cgl-15 {
  width: 100%;
  min-height: 100vh;
  --mint: oklch(0.85 0.12 165);
  --ink: oklch(0.24 0.03 180);
  --bg: oklch(0.2 0.03 175);
  font-family: system-ui,sans-serif;
  background: var(--bg);
  padding: 1.4rem;
}

.cgl-15 .cgl-15__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(min(190px,100%),1fr));
  grid-auto-rows: auto;
  gap: 14px;
}

.cgl-15 .cgl-15__card {
  grid-row: span 4;
  display: grid;
  grid-template-rows: subgrid;
  row-gap: .6rem;
  background: oklch(0.25 0.03 175);
  border: 1px solid color-mix(in oklab,var(--mint) 25%,transparent);
  border-radius: 16px;
  padding: 1.2rem;
}

.cgl-15 .cgl-15__card--hot {
  background: color-mix(in oklab,var(--mint) 14%,oklch(0.25 0.03 175));
  border-color: var(--mint);
}

.cgl-15 .cgl-15__eyebrow {
  font-size: .66rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--mint);
}

.cgl-15 h4 {
  font-size: 1.05rem;
  line-height: 1.3;
  color: white;
  text-wrap: balance;
}

.cgl-15 .cgl-15__desc {
  font-size: .8rem;
  line-height: 1.6;
  color: oklch(0.75 0.02 175);
}

.cgl-15 .cgl-15__btn {
  align-self: end;
  justify-self: start;
  font-size: .82rem;
  font-weight: 600;
  color: var(--ink);
  background: var(--mint);
  text-decoration: none;
  padding: .55rem 1rem;
  border-radius: 999px;
  min-block-size: 2.4rem;
  display: inline-grid;
  align-items: center;
}

.cgl-15 .cgl-15__btn:hover {
  background: color-mix(in oklab,var(--mint) 85%,white);
}

.cgl-15 .cgl-15__btn:focus-visible {
  outline: 2px solid var(--mint);
  outline-offset: 3px;
}

@supports not (grid-template-rows: subgrid) {
  .cgl-15 .cgl-15__card {
    grid-template-rows: auto auto 1fr auto;
  }
}
/* No JavaScript — grid-template-rows: subgrid makes all cards share the parent's row tracks, aligning every section across cards. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Grid Layout from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Subgrid Card
Source: https://codefronts.com/layouts/css-grid-layouts/subgrid-card/

The card-alignment problem, finally solved: titles of different lengths, descriptions of different depths — yet every title baseline, every description block and every button aligns PERFECTLY across cards, because each card inherits the parent's row tracks with grid-template-rows: subgrid. The demo shows misaligned vs aligned side by side.
## HTML
```html
<section class="cgl-15" aria-label="Subgrid card alignment demo">
  <div class="cgl-15__grid">
    <article class="cgl-15__card">
      <p class="cgl-15__eyebrow">Starter</p>
      <h4>Ship your first project</h4>
      <p class="cgl-15__desc">Every card spans the same four parent rows.</p>
      <a href="#0" class="cgl-15__btn">Choose Starter</a>
    </article>
    <article class="cgl-15__card cgl-15__card--hot">
      <p class="cgl-15__eyebrow">Studio · most popular</p>
      <h4>A much longer title that wraps onto two full lines</h4>
      <p class="cgl-15__desc">The title track grows for the tallest title — and every sibling's title track grows with it. That is subgrid.</p>
      <a href="#0" class="cgl-15__btn">Choose Studio</a>
    </article>
    <article class="cgl-15__card">
      <p class="cgl-15__eyebrow">Agency</p>
      <h4>Scale it</h4>
      <p class="cgl-15__desc">Short title, short copy — the button still lands on the shared final track, perfectly level with its neighbours.</p>
      <a href="#0" class="cgl-15__btn">Choose Agency</a>
    </article>
  </div>
</section>
```
## CSS
```css
.cgl-15,
.cgl-15 *,
.cgl-15 *::before,
.cgl-15 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cgl-15 {
  width: 100%;
  min-height: 100vh;
  --mint: oklch(0.85 0.12 165);
  --ink: oklch(0.24 0.03 180);
  --bg: oklch(0.2 0.03 175);
  font-family: system-ui,sans-serif;
  background: var(--bg);
  padding: 1.4rem;
}

.cgl-15 .cgl-15__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(min(190px,100%),1fr));
  grid-auto-rows: auto;
  gap: 14px;
}

.cgl-15 .cgl-15__card {
  grid-row: span 4;
  display: grid;
  grid-template-rows: subgrid;
  row-gap: .6rem;
  background: oklch(0.25 0.03 175);
  border: 1px solid color-mix(in oklab,var(--mint) 25%,transparent);
  border-radius: 16px;
  padding: 1.2rem;
}

.cgl-15 .cgl-15__card--hot {
  background: color-mix(in oklab,var(--mint) 14%,oklch(0.25 0.03 175));
  border-color: var(--mint);
}

.cgl-15 .cgl-15__eyebrow {
  font-size: .66rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--mint);
}

.cgl-15 h4 {
  font-size: 1.05rem;
  line-height: 1.3;
  color: white;
  text-wrap: balance;
}

.cgl-15 .cgl-15__desc {
  font-size: .8rem;
  line-height: 1.6;
  color: oklch(0.75 0.02 175);
}

.cgl-15 .cgl-15__btn {
  align-self: end;
  justify-self: start;
  font-size: .82rem;
  font-weight: 600;
  color: var(--ink);
  background: var(--mint);
  text-decoration: none;
  padding: .55rem 1rem;
  border-radius: 999px;
  min-block-size: 2.4rem;
  display: inline-grid;
  align-items: center;
}

.cgl-15 .cgl-15__btn:hover {
  background: color-mix(in oklab,var(--mint) 85%,white);
}

.cgl-15 .cgl-15__btn:focus-visible {
  outline: 2px solid var(--mint);
  outline-offset: 3px;
}

@supports not (grid-template-rows: subgrid) {
  .cgl-15 .cgl-15__card {
    grid-template-rows: auto auto 1fr auto;
  }
}
```

## JavaScript
```js
/* No JavaScript — grid-template-rows: subgrid makes all cards share the parent's row tracks, aligning every section across cards. */
```

How this works

The parent grid declares grid-auto-rows: auto and the cards span four rows: grid-row: span 4 with display: grid; grid-template-rows: subgrid. Now the eyebrow, title, description and button of EVERY card share the same four parent tracks — each track sized by the tallest instance across the whole row. A two-line title in card 2 makes the title track taller for all cards; buttons all sit on the final track's baseline.

This is impossible with nested independent grids (each card would size its own rows) and hacky with flexbox (margin-top:auto only pins the LAST element). Subgrid is the first mechanism where siblings' internals align to each other. The gap is inherited from the parent but can be overridden locally.

Make it yours

  • Add a card section (image, price row) by increasing the span AND adding one element per card.
  • Cap description length with -webkit-line-clamp if a single rambling card making all cards tall is unacceptable.
  • Works identically for pricing tables, product cards, team bios — anything repeated.
  • The columns can subgrid too (grid-template-columns: subgrid) for icon+text alignment inside rows.

Gotchas — read before shipping

  • The span count must equal the number of card children exactly — a forgotten element shifts every subsequent track for that card only.
  • Subgrid tracks are the PARENT's: you cannot resize one card's title track locally. That rigidity is the feature.
  • Test the @supports fallback — pre-subgrid Safari 15 users get independently-sized cards (graceful, but check it).

Browser support

ChromeSafariFirefoxEdge
117+16+71+117+

subgrid is fully interoperable since Sept 2023 — Baseline 2023. The @supports fallback covers the stragglers.

Techniques used in this demo

Search CodeFronts

Loading…