Perfectly Aligned Card Layouts with CSS Subgrid

Posted by Dustin Boston on in .

Recently, I’ve been updating the plans page at work. The design is a row of cards, and each card shows a plan, some details, and a price. Every element of one card has to line up in a row with elements in the other cards.

The content has been challenging. Some elements are always there, while others might not be. Some elements can grow, while others are static.

My first approach was to add placeholder elements for every row. Placeholders worked as long as the text was the expected height. But if the text were too long, it would knock the other rows out of alignment.

The problem was that each card’s grid was independent of the others. I needed them to share a single, unified row structure. This is the exact problem CSS Subgrid solves.

So I tried a new way. Using a list structure, the parent UL is where I defined the rows, and the LI is the subgrid. Using subgrid, the cards inherit the tracks (or rows) from the parent.

Now, when the longer promo text expanded its row, it expanded the primary grid’s row. Because every card was borrowing that same primary grid, all rows grew together. Check out the following code to see how it works.

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>CSS Subgrid Cards</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <!-- Main grid container - defines the shared row structure -->
    <div class="cards-container">
      <!-- Each card becomes a subgrid that inherits the parent's rows -->
      <div class="card">
        <!-- Row 1: Optional badge -->
        <div class="card-badge">New!</div>
        <!-- Row 2: Logos section -->
        <div class="card-logos">
          <img src="https://via.placeholder.com/30" alt="Logo 1" />
          <img src="https://via.placeholder.com/30" alt="Logo 2" />
        </div>
        <!-- Row 3: Promo text (can vary in length) -->
        <div class="card-promo">
          Save 20% on your first purchase. This promo text is short.
        </div>
        <!-- Row 4: Price -->
        <div class="card-price">$49.99</div>
        <!-- Row 5: Description (flexible height with 1fr) -->
        <div class="card-description">This is a short product description.</div>
        <!-- Row 6: Terms link (always at bottom) -->
        <a href="#" class="card-terms">Terms & Conditions</a>
      </div>

      <div class="card">
        <!-- This card has no badge, but still aligns with others -->
        <!-- Row 2: Logos section -->
        <div class="card-logos">
          <img src="https://via.placeholder.com/30" alt="Logo A" />
          <img src="https://via.placeholder.com/30" alt="Logo B" />
        </div>
        <!-- Row 3: Longer promo text - will expand row 3 for ALL cards -->
        <div class="card-promo">
          This promo text is longer than the others and spans over multiple
          lines to demonstrate how subgrid keeps everything aligned.
        </div>
        <!-- Row 4: Price -->
        <div class="card-price">$29.50</div>
        <!-- Row 5: Longer description -->
        <div class="card-description">
          This description is also a bit longer to test the `1fr` behavior,
          ensuring the terms link stays at the bottom.
        </div>
        <!-- Row 6: Terms link -->
        <a href="#" class="card-terms">Terms & Conditions</a>
      </div>

      <div class="card">
        <!-- Row 1: Different badge text -->
        <div class="card-badge">Best Seller</div>
        <!-- Row 2: Single logo -->
        <div class="card-logos">
          <img src="https://via.placeholder.com/30" alt="Logo X" />
        </div>
        <!-- No promo text - row 3 still maintains height from other cards -->
        <!-- Row 4: Price -->
        <div class="card-price">$75.00</div>
        <!-- No description - row 5 still maintains minimum space -->
        <!-- Row 6: Terms link -->
        <a href="#" class="card-terms">Terms & Conditions</a>
      </div>
    </div>
  </body>
</html>

style.css

body {
  font-family: sans-serif;
  padding: 2rem;
  background-color: #f0f2f5;
}

/* MAIN GRID: The Main Row Controller */
.cards-container {
  display: grid;

  /* Create equal-width columns for each card */
  grid-auto-columns: 1fr;
  grid-auto-flow: column;
  gap: 1.5rem;

  /* Visual styling */
  padding: 1rem;
  background-color: #fff;
  border-radius: 12px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);

  /* ⭐ THE SUBGRID SECRET ⭐ */
  /* Define 6 rows that ALL cards will share:
     Row 1: auto (badge) - shrinks to content or disappears
     Row 2: auto (logos) - shrinks to content
     Row 3: auto (promo) - grows with longest promo text
     Row 4: auto (price) - shrinks to content
     Row 5: 1fr (description) - takes remaining space
     Row 6: auto (terms) - shrinks to content, stays at bottom */
  grid-template-rows: auto auto auto auto 1fr auto;
}

/* SUBGRID: Each Card Inherits the Main Rows */
.card {
  display: grid;

  /* Span all 6 rows defined by the parent */
  grid-row: 1 / -1;

  /* 🎯 THE MAGIC HAPPENS HERE 🎯 */
  /* Instead of creating its own rows, this card inherits
     the exact same 6 rows from .cards-container */
  grid-template-rows: subgrid;

  /* Visual styling */
  padding: 1rem;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  background-color: #ffffff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  transition: transform 0.2s;
}

.card:hover {
  transform: translateY(-5px);
}

/* CARD ELEMENTS: Each Assigned to Specific Inherited Rows */

/* Row 1: Badge (optional - some cards don't have this) */
.card-badge {
  grid-row: 1; /* Uses inherited row 1 from parent */
  background-color: #4caf50;
  color: #fff;
  padding: 0.2rem 0.5rem;
  border-radius: 4px;
  width: fit-content;
  font-size: 0.8rem;
  font-weight: bold;
}

/* Row 2: Logos section */
.card-logos {
  grid-row: 2; /* Uses inherited row 2 from parent */
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.card-logos img {
  height: 30px;
  width: 30px;
}

/* Row 3: Promo text (this can vary greatly in length) */
.card-promo {
  grid-row: 3; /* Uses inherited row 3 from parent */
  color: #007bff;
  font-weight: bold;
  /* When this text is long in one card, it expands row 3
     for ALL cards because they share the same grid rows */
}

/* Row 4: Price */
.card-price {
  grid-row: 4; /* Uses inherited row 4 from parent */
  font-size: 1.5rem;
  font-weight: bold;
  color: #333;
}

/* Row 5: Description (uses 1fr from parent - takes remaining space) */
.card-description {
  grid-row: 5; /* Uses inherited row 5 from parent */
  color: #666;
  margin-top: 0.5rem;
  /* This row gets 1fr, so it expands to fill available space,
     pushing the terms link to the bottom */
}

/* Row 6: Terms link (always at the bottom) */
.card-terms {
  grid-row: 6; /* Uses inherited row 6 from parent */
  align-self: end; /* Stick to bottom of this row */
  margin-top: 1rem;
  font-size: 0.8rem;
  color: #999;
  text-decoration: none;
}

.card-terms:hover {
  text-decoration: underline;
}

/* 
💡 HOW SUBGRID WORKS:
1. Parent (.cards-container) defines 6 rows with specific sizing
2. Each card (.card) uses 'grid-template-rows: subgrid' to inherit those exact rows
3. When content in one card expands a row, that row expands for ALL cards
4. Elements stay perfectly aligned across all cards, regardless of content length
*/