You have cards with different content lengths. CSS Grid makes them all the same height per row, leaving ugly empty space. Here's how to fix that with pure CSS.
The Problem: Grid = Equal Row Heights
With CSS Grid, cards in the same row are forced to the same height:
CSS Grid:
┌──────────────┐ ┌──────────────┐
│ Short card │ │ Tall card │
│ │ │ with lots of │
│ (wasted │ │ content that │
│ space!) │ │ fills the │
│ │ │ whole card │
└──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐
│ Medium card │ │ Tiny │
│ with some │ │ │
│ content │ │ (wasted │
│ │ │ space!) │
└──────────────┘ └──────────────┘
The short card stretches to match its tall neighbor. Wasted space everywhere.
Copy this into an HTML file and see it yourself:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Problem</title>
<style>
body { font-family: system-ui, sans-serif; padding: 2rem; background: #f5f5f5; }
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.card {
padding: 1.5rem;
background: white;
border-radius: 0.75rem;
border: 1px solid #e5e5e5;
}
</style>
</head>
<body>
<h1>The Problem: CSS Grid</h1>
<p style="margin-bottom: 1rem; color: #666;">Notice how short cards stretch to match tall ones in the same row.</p>
<div class="grid">
<div class="card">
<h3>Short card</h3>
<p>Just a few words.</p>
</div>
<div class="card">
<h3>Tall card</h3>
<p>This card has way more content. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum. Cras vehicula risus quis vulputate arcu.</p>
</div>
<div class="card">
<h3>Medium card</h3>
<p>Some content here, not too much.</p>
</div>
<div class="card">
<h3>Tiny card</h3>
<p>Hi.</p>
</div>
</div>
</body>
</html>
The Fix: CSS Columns
Replace the grid with two CSS properties and each card takes only the height it needs:
CSS Columns:
┌──────────────┐ ┌──────────────┐
│ Short card │ │ Tall card │
└──────────────┘ │ with lots of │
┌──────────────┐ │ content that │
│ Medium card │ │ fills the │
│ with some │ │ whole card │
│ content │ └──────────────┘
└──────────────┘ ┌──────────────┐
┌──────────────┐ │ Tiny │
│ Another one │ └──────────────┘
└──────────────┘
No wasted space! Each card = its own height.
Here's the code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSS Masonry</title>
<style>
body { font-family: system-ui, sans-serif; padding: 2rem; background: #f5f5f5; }
.masonry {
columns: 2; /* split into 2 columns */
gap: 1rem;
}
.card {
break-inside: avoid; /* don't split a card across columns */
margin-bottom: 1rem;
padding: 1.5rem;
background: white;
border-radius: 0.75rem;
border: 1px solid #e5e5e5;
}
@media (max-width: 640px) {
.masonry { columns: 1; }
}
</style>
</head>
<body>
<h1>The Fix: CSS Columns</h1>
<p style="margin-bottom: 1rem; color: #666;">Each card takes only the height it needs. No stretching.</p>
<div class="masonry">
<div class="card">
<h3>Short card</h3>
<p>Just a few words.</p>
</div>
<div class="card">
<h3>Tall card</h3>
<p>This card has way more content. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum. Cras vehicula risus quis vulputate arcu.</p>
</div>
<div class="card">
<h3>Medium card</h3>
<p>Some content here, not too much.</p>
</div>
<div class="card">
<h3>Tiny card</h3>
<p>Hi.</p>
</div>
<div class="card">
<h3>Another tall one</h3>
<p>More content to show the layout handles varying heights gracefully. Cards flow top-to-bottom in each column, like reading a newspaper.</p>
</div>
<div class="card">
<h3>Last one</h3>
<p>Done!</p>
</div>
</div>
</body>
</html>
That's it. columns: 2 + break-inside: avoid. Done.
But Wait: Expandable Cards Break It
If your cards have a "Read more" button that expands content, CSS columns rebalance all cards when one changes height. Everything shifts around:
BEFORE clicking "Read more": AFTER clicking "Read more":
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Card 1 │ │ Card 3 │ │ Card 1 │ │ Card 4 │
│ [Read more] │ └──────────────┘ │ now expanded │ │ [Read more] │
└──────────────┘ ┌──────────────┐ │ with all the │ └──────────────┘
┌──────────────┐ │ Card 4 │ │ full text │ ┌──────────────┐
│ Card 2 │ │ [Read more] │ │ showing... │ │ Card 5 │
└──────────────┘ └──────────────┘ │ [Show less] │ └──────────────┘
┌──────────────┐ ┌──────────────┐ └──────────────┘
│ Card 3 ←─── jumped from │ ┌──────────────┐
└──────────────┘ right column! │ Card 2 │
└──────────────┘
← Cards 2-5 all moved!
Try it yourself:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Reflow Bug</title>
<style>
body { font-family: system-ui, sans-serif; padding: 2rem; background: #f5f5f5; }
.masonry {
columns: 2;
gap: 1rem;
}
.card {
break-inside: avoid;
margin-bottom: 1rem;
padding: 1.5rem;
background: white;
border-radius: 0.75rem;
border: 1px solid #e5e5e5;
}
.card-text {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-text.clamped {
-webkit-line-clamp: 2;
}
button {
margin-top: 0.5rem;
background: none;
border: none;
color: #3b82f6;
cursor: pointer;
font-size: 0.875rem;
}
</style>
</head>
<body>
<h1>The Bug: Cards Shift on Expand</h1>
<p style="margin-bottom: 1rem; color: #666;">Click "Read more" and watch ALL cards jump around.</p>
<div class="masonry">
<div class="card">
<h3>Card 1</h3>
<p class="card-text clamped">This card has a lot of text that gets clamped. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum vestibulum. Cras vehicula risus.</p>
<button onclick="this.previousElementSibling.classList.toggle('clamped')">Read more</button>
</div>
<div class="card">
<h3>Card 2</h3>
<p>Short card. Watch me jump when you expand Card 1!</p>
</div>
<div class="card">
<h3>Card 3</h3>
<p>I'll move too.</p>
</div>
<div class="card">
<h3>Card 4</h3>
<p class="card-text clamped">Another long card. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum vestibulum.</p>
<button onclick="this.previousElementSibling.classList.toggle('clamped')">Read more</button>
</div>
<div class="card">
<h3>Card 5</h3>
<p>Yet another card that will shift.</p>
</div>
</div>
</body>
</html>
Annoying, right? Here's the fix.
The Real Fix: Split Into Separate Columns
Instead of letting CSS decide which card goes where, you split them into independent columns. Now expanding a card only affects its own column:
BEFORE clicking "Read more": AFTER clicking "Read more":
Column 1 Column 2 Column 1 Column 2
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Card 1 │ │ Card 2 │ │ Card 1 │ │ Card 2 │
│ [Read more] │ │ [Read more] │ │ now expanded │ │ [Read more] │
└──────────────┘ └──────────────┘ │ with all the │ └──────────────┘
┌──────────────┐ ┌──────────────┐ │ full text │ ┌──────────────┐
│ Card 3 │ │ Card 4 │ │ showing... │ │ Card 4 │
└──────────────┘ └──────────────┘ │ [Show less] │ └──────────────┘
┌──────────────┐ └──────────────┘
│ Card 5 │ ┌──────────────┐
└──────────────┘ │ Card 3 │ ← only Card 3
└──────────────┘ moved down
┌──────────────┐
│ Card 5 │ Column 2 is
└──────────────┘ untouched!
Each column is its own flex container. They don't know about each other.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stable Masonry</title>
<style>
body { font-family: system-ui, sans-serif; padding: 2rem; background: #f5f5f5; }
.masonry {
display: flex;
gap: 1rem;
}
.column {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
}
.card {
padding: 1.5rem;
background: white;
border-radius: 0.75rem;
border: 1px solid #e5e5e5;
}
.card-text {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-text.clamped {
-webkit-line-clamp: 2;
}
button {
margin-top: 0.5rem;
background: none;
border: none;
color: #3b82f6;
cursor: pointer;
font-size: 0.875rem;
}
@media (max-width: 640px) {
.masonry { flex-direction: column; }
}
</style>
</head>
<body>
<h1>The Fix: Independent Columns</h1>
<p style="margin-bottom: 1rem; color: #666;">Click "Read more" — only the same column moves. The other stays put.</p>
<div class="masonry">
<!-- Column 1: items 1, 3, 5 -->
<div class="column">
<div class="card">
<h3>Card 1</h3>
<p class="card-text clamped">This card has a lot of text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum vestibulum. Cras vehicula risus.</p>
<button onclick="this.previousElementSibling.classList.toggle('clamped')">Read more</button>
</div>
<div class="card">
<h3>Card 3</h3>
<p>I stay still when Card 2 expands!</p>
</div>
<div class="card">
<h3>Card 5</h3>
<p>Me too.</p>
</div>
</div>
<!-- Column 2: items 2, 4 -->
<div class="column">
<div class="card">
<h3>Card 2</h3>
<p class="card-text clamped">Another long card. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum vestibulum.</p>
<button onclick="this.previousElementSibling.classList.toggle('clamped')">Read more</button>
</div>
<div class="card">
<h3>Card 4</h3>
<p>Short and stable.</p>
</div>
</div>
</div>
</body>
</html>
How it works: odd items (1, 3, 5) go in column 1, even items (2, 4) go in column 2. Each column is a separate flex container, so they don't affect each other.
Quick Reference
Need masonry?
│
├── Cards have fixed content?
│ └── ✅ Use CSS columns + break-inside: avoid
│
├── Cards expand/collapse?
│ └── ✅ Split into separate flex columns
│
└── All cards same height?
└── ✅ Just use CSS Grid (no masonry needed)
Three examples you can copy-paste and try right now. That's the whole trick — no libraries needed!
Top comments (0)