CSS Grid, maintaining aspect ratio and managing overflow

We’ve recently deprecated IE support for our product and this has opened up a world of new possibilities when it comes to development. While I know CSS Grid had partial support in IE (Microsoft created the original spec), the full implementation is much more magical. Patience is a virtue.

Speaking of changes, we’re in the middle of a product redesign and some of the new looks call for some cards on summary pages. These cards have a few requirements:

  1. The number of cards must act responsively, filling the width of the container in some percentage until hitting a threshold where a greater or fewer amount of cards can fit.
  2. Each card will have a main image on the top, which must maintain a 4:3 aspect ratio no matter the width of the card (this means the width will affect the height).
  3. There should only ever be one row of cards.

So let’s get started!

Responsive cards using CSS Grid

For the first requirement, we could potentially use either CSS Flexbox or CSS Grid. Looking at the third requirement, we’d probably assume that we want to use Flexbox because we only ever want to have one row. However, if there are more cards than space, we’ll have some issues with the container. For example, if we don’t use flex-wrap: wrap;, each card is going to squish to fit the items inside the row of the container; it will not overflow outside of it. On the other hand, if we do use wrap, we’ll have difficulty managing how the cards appear on the first row. We’d need a min-width and a max-width threshold, and even then we will find certain container widths with an odd gap on the end.

A much better solution would be to use CSS Grid with just a few lines of code:

:root {
--spacing: 24px;
--min-card-width: 250px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--min-card-width), 1fr));
grid-column-gap: var(--spacing);
}

The variables are there so that I can easily tweak the values. This will solve everything in the first requirement nicely.

Aspect ratio using padding percentage

The next step is an oldie but a goody. CSS-Tricks has a good post on this. Each grid item will need to maintain an aspect ratio.

:root {
--ratio-percent: 75%;
}
.aspect-ratio {
padding-top: var(--ratio-percent);
}
.absolute-fill {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

The width of these items is determined by the grid; and I’m setting the padding to a variable at the top of the file just in case we’d need to change from a 4:3 to some other ratio easily. Then each child of the grid item will have a fill on it. Notice I haven’t set position: relative; on the .aspect-ratio class in order for the fill to affect the children properly; we’ll need to do something about that in a bit.

So all of this is good, but we have one final requirement we need to meet.

Hiding overflow using… a lot of stuff

Our final requirement is only showing one row of cards. Since we’ve chosen the CSS Grid implementation of the cards, you could start by using grid-row: 1; on the children of the grid. However, doing this will give you the same issue we had when using Flexbox; everything will be stuck on the row and squished together.

What we really want to do is make the grid container as tall as a single card. However, the size of the grid is based on the number of items within it. Even if we could set the height, the height of the card changes based on the width of the cards, and the width of a card changes based on the width of the container. Yeesh! How do we manage all of this?

First, let me provide some markup. It should give you a bit of a hint.

<section class="grid">
<aside class="aspect-ratio"></aside>
<article>
<ul class="grid absolute-fill">
<li class="aspect-ratio">
<div class="absolute-fill">content 1</div>
</li>
<li class="aspect-ratio">
<div class="absolute-fill">content 2</div>
</li>
<li class="aspect-ratio">
<div class="absolute-fill">content 3</div>
</li>
<li class="aspect-ratio">
<div class="absolute-fill">content 4</div>
</li>
<li class="aspect-ratio">
<div class="absolute-fill">content 5</div>
</li>
</ul>
</article>
</section>

If you look closely, you’ll see there are two uses of the .grid class. One is on a parent container which holds two items: a <aside class="aspect-ratio"/>, and a wrapper for another .grid element. The idea is this:

Both .grid elements will have the same settings from the class above, meaning their children will act just the same under the grid-template-columns rule we set earlier. The <aside class="aspect-ratio"/> element will act like a card but is empty. The purpose of this is to determine the height of its sibling; the <article/> element. We can fill the entire grid with this item by setting grid-row and grid-column to 1 / -1; for both. The <article/> element can then provide a height to its children, specifically the nested <ul class="grid absolute-fill"/> inside of it. This will become the dimensions of the parent, and the height of that dimension was based off of a card.

Adding it all together, including some of the necessary position: relative; rules in the right places, we should get a solid result. You can see the full styles below:

*, *:before, *:after {
box-sizing: border-box;
}
:root {
/* variables */
--spacing: 24px;
--min-card-width: 250px;
--ratio-percent: 75%;
--addl-height: 100px;
}
body {
margin: 0;
}
/* just for the purposes of the demo */
header, footer, section, div {
border: 1px solid blue;
padding: var(--spacing);
}
section {
/* hide all the overflowing cards */
overflow: hidden;
}
article {
grid-row: 1 / -1;
grid-column: 1 / -1;
position: relative;
}
ul {
/* clear ul styles */
list-style: none;
margin: 0;
padding: 0;

/* additional gap */
grid-row-gap: var(--spacing);
}
li {
/* set up aspect ratio hack */
position: relative;
}
.absolute-fill {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--min-card-width), 1fr));
grid-column-gap: var(--spacing);
}
.aspect-ratio {
padding-top: var(--ratio-percent);
}
.aspect-ratio:after {
content:"";
height: var(--addl-height);
display: block;
border-top: 1px solid blue;
}
The final result (with a header and footer)

We’re using the :after element just to show what card content below the card might look like but this could easily be another element. The catch is that the size of that content must be included in the spacer element in the top-level grid (the <aside/> element in this case). That element’s height dictates the height of the grid container, not the content cards themselves.

I’m pretty sure you could use a pseudo element instead of the <aside/>, but I wanted to show the relationship between it and the cards in the example.

You can see this in action here when you resize the browser.