Sizing images based on their aspect ratio
Grids of logos are my nightmare.
I mean, never mind the opportunity to learn the bewildering, but admittedly finite, array of file formats in which visual assets can conceivably be stored and distributed. But the intricate dance of making all logos look equally prominent in the grid is something for which I can only muster very little enthusiasm.
Like with all tedious things, I've found myself wishing more than once that CSS included a declarative way to make images of various aspect ratios look the same size.
Making images „look the same size”
A definition of that might be to make the images occupy the same surface area. Let's work out the math.
We want to keep constant the area of an image of natural width W
and height H
. To that end, we must find its preferred dimensions Wp
and Hp
so that Wp × Hp
is uniform across images. The givens:
area = Wp * Hp;
aspect_ratio = W / H = Wp / Hp;
…ultimately lead to the formula below. (Individual steps are left as an exercise to the reader.)
Wp = sqrt(area) * sqrt(W / H);
Let's express this formula in HTML and CSS.
You should already have explicit width
and height
attributes on <img>
elements in your markup to define the intrinsic size of the image and prevent layout shifts as the image loads. The values of these attributes cannot be used in CSS directly, so we also pass them as custom properties via the style
attribute.
<div class='ratio-gallery'>
<img
src='https://source.unsplash.com/random/800x600'
alt='800 by 600 pixels placeholder image'
width='800'
height='600'
style='--w: 800; --h: 600'
/>
</div>
Note that we're passing the width
and height
as unitless numbers. This gives us the most flexibility when using the values in CSS math functions. The sqrt()
function, in particular, only accepts <number>
s. Our basic CSS looks like this:
.ratio-gallery {
--basis: 300px;
}
.ratio-gallery img {
--w: 1;
--h: 1;
width: calc(var(--basis) * sqrt(var(--w) / var(--h)));
height: auto;
}
Instead of defining an --area
constant like in our math formula, we use its square root directly as --basis
. This has the added benefit of being more intuitive: you can think of its value as making the image as large as a square of that side length. For example, --basis: 300px
makes the image as large as a 300×300
square.
That's pretty much all there is to the technique. Below are some images sized with it, placed in a flexbox container. In a browser that supports the sqrt()
CSS function — which, at the moment of writing, is just Safari — you should see three images of identical surface areas:
Fallback to a fixed aspect ratio
The following @supports
at-rule will check if the browser supports the sqrt()
CSS function:
/*
To detect support for `sqrt()` we use `flex`,
the shortest (afaict) CSS property that accepts
the `<number>` value that `sqrt()` produces.
*/
@supports (flex: sqrt(0)) {
}
In browsers that don't support this function, we could enforce a fixed aspect-ratio
and use the object-fit
property to clip the image to fit, as described by Michelle Barker. To make the fallback comparable, we do have to precompute the square root of this fixed aspect ratio and factor it in — not ideal, but not exceedingly annoying. Here's the technique along with the fallback:
.ratio-gallery {
--basis: 300px;
}
.ratio-gallery img {
--w: 1;
--h: 1;
width: calc(var(--basis) * sqrt(var(--w) / var(--h)));
height: auto;
}
@supports not (flex: sqrt(0)) {
.ratio-gallery img {
width: calc(var(--basis) * 1.224 /* = sqrt(3/2) */);
aspect-ratio: 3/2;
object-fit: cover;
}
}
Here is a more developed demo:
Wrapping up
Turns out aspect-ratio-aware sizing is not too hard to pull off as long as we have control of image dimensions in the markup. The technique may be especially useful in content management systems where authors are able to upload arbitrary assets that need to look good next to each other, and where the author is not expected, or is unable, to fine-tune the sizes via custom CSS.
Addenda
Defensive CSS
As Šime helpfully points out, the basic technique does not handle extreme aspect ratios well. In fact, the arbitrary in arbitrary aspect ratio is the cue to put our defensive CSS hat on.
We can limit the space the image takes in any one direction with max-width
and max-height
:
.ratio-gallery img {
--w: 1;
--h: 1;
width: calc(var(--basis) * sqrt(var(--w) / var(--h)));
height: auto;
max-width: calc(var(--basis) * 2);
max-height: calc(var(--basis) * 2);
}
(You might also want to throw in an object-fit: cover
to handle very tall images that get squished when max-height
kicks in.)
Here is the demo again, which includes the defensive CSS:
Prior art
Update Dec. 2022: Piper Haywood had described back in 2020 a technique for sizing images using the sqrt()
CSS function, at a time when the function was not yet supported in any browser. Nick Sherman produced a nice demo for the technique, using an approximation of sqrt()
.