A card grid is the layout most often built with four breakpoints and a lot of arithmetic. Grid removes most of that: one declaration produces a sensible number of columns at any width, and the browser decides where the breaks fall.
How do you build it with one rule?
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1.5rem;
}
That is the whole grid. The browser works out how many 16rem columns fit in the available width, creates that many tracks, and shares the leftover space between them. Narrow the window and the number of columns drops; widen it and they increase. There is no breakpoint to choose, because the only constraint is the minimum width a card should have.
Choosing the minimum is the real design decision. 16rem is a reasonable floor for a card with a heading and a short paragraph; if the cards contain an image with text below it, something closer to 20rem reads better. Too small and the cards become unreadably narrow, too large and you get one lonely card on a wide screen.
When should you use auto-fill?
The difference only shows when there are fewer items than the grid can hold. auto-fit collapses the empty tracks and the items stretch to fill the row. auto-fill keeps the empty tracks, so the items keep their maximum width and the row is left ragged.
/* three cards, room for four */
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); /* three cards fill the row */
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); /* three cards stay 16rem, gap at the end */
For a card grid, auto-fit is usually what you want: the cards fill the available width and look deliberate. For a gallery where each item should keep a fixed size regardless of how many there are, auto-fill is the right one.
What about the last row?
This is the weakness of the auto-fit approach. A row of three cards in a four-column grid leaves a gap, and the gap is unavoidable with this technique — the cards do not grow to fill it, because the column count is decided by the width, not by the number of items.
/* if the last row must be flush, size the tracks by the item count instead */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
}
The min() is there for narrow screens: it stops the minimum from being wider than the container, which would cause an overflow on a small phone. On a very narrow viewport the track becomes as wide as the container, and the cards stack in a single column.
If a flush last row is a hard requirement, the alternatives are a flex row with wrapping, where the items share the space between them, or an explicit column count at each breakpoint. Both cost more than the ragged edge is usually worth.
What does a card need to look right?
The grid handles the layout; the card itself needs a little care so that cards of different content lengths still look like a set.
.card {
display: flex;
flex-direction: column; /* so the footer can be pushed to the bottom */
gap: 0.75rem;
padding: 1.25rem;
border: 1px solid #e2e2e2;
border-radius: 10px;
background: #fff;
}
.card h3 { margin: 0; }
.card p {
margin: 0;
color: #5c5c5c;
}
.card .more {
margin-top: auto; /* pushes the link to the bottom of the card */
}
Two details do the work. Making the card a flex column lets the margin-top: auto on the last element push it to the bottom, so the "read more" links line up across a row of cards with different text lengths. And the grid's default align-items: stretch already makes every card in a row the same height, so the borders line up.
One thing to avoid is a fixed height on the card. It clips longer content, and the grid will happily make the row taller if the cards are allowed to grow.
What can you add to a card grid?
A featured card that spans two columns is one line, and it is the usual first thing a design calls for.
.card--featured {
grid-column: span 2;
}
On a narrow screen, a card that spans two columns may not fit, and the grid will handle it by creating a narrower track instead. If you want the featured card to become a normal card below a certain width, a media query with grid-column: auto does it.
How do you make the cards in a row the same height?
A grid row is as tall as its tallest item, and that is a feature rather than a problem. Every card in the row stretches to match, so the buttons along the bottom line up and the row reads as a set instead of a pile.
The default is align-items: stretch, which fills each cell with the item. If you have set align-items: start somewhere - to stop a short item from stretching - the cards will be different heights again, and the fix is to remove that override.
<div class="cards">
<article class="card">
<h3>Short title</h3>
<p>One line of text.</p>
<a class="card__cta" href="#">Start</a>
</article>
<article class="card">
<h3>A much longer title that wraps onto two lines</h3>
<p>Several lines of text, enough to make this card considerably taller than the one beside it.</p>
<a class="card__cta" href="#">Start</a>
</article>
</div>.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: 1rem;
}
.card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
border: 1px solid #d8e0e8;
border-radius: 10px;
}
/* the paragraph takes the leftover height, pushing the link to the bottom */
.card p {
margin: 0;
flex: 1;
}
.card__cta {
align-self: flex-start;
padding: 0.4rem 0.8rem;
background: #10243a;
color: #ffffff;
border-radius: 6px;
text-decoration: none;
}Two things are working together here. The grid stretches the cells so the row has one height, and the flex column inside each card pushes the link to the bottom by letting the paragraph grow. Without the flex: 1 on the paragraph, the link would sit directly under the text and the buttons would be at different heights again.
When the cards are not in a grid but in a flex row, the same result comes from align-items: stretch on the row - which is the default - combined with the same flex column inside each card.
Quick reference
repeat(auto-fit, minmax(16rem, 1fr))is a responsive card grid with no breakpoints.- The minimum is the design decision: too small is unreadable, too large leaves one lonely card.
auto-fitstretches the items;auto-fillkeeps them at their size.- Wrap the minimum in
min(16rem, 100%)so narrow screens do not overflow. - Make the card a flex column so
margin-top: autoaligns the footer links. - Grid stretches the cards in a row to equal heights, so do not set a fixed height.
Try it yourself: build a card grid with one rule and resize the window, counting the columns as it goes. Add a third card and watch the last row. Then make one card's text twice as long as the others and confirm the links still line up, because the card is a flex column with an auto margin.