Every once in a while, I have to figure out how to display images on the web with a consistent surface area. It’s usually in relation to making a lot of logos with very different aspect ratios look evenly sized so that none of them stick out like a sore thumb.
I haven’t had to do this in a while but came across a tweet by Nick Sherman that prompted me to think about it again.
To achieve an evenly-sized group of images, you have to calculate a maximum width that is proportionate to the surface area you want. To do this, you need to be able to calculate a square root, you need the width and height of each original image, and all of your images need to be tightly cropped since extra negative space will throw things off visually.
In the past, I’ve achieved this with logic since vanilla CSS doesn’t currently support a sqrt()
function (more on this later). I’ve usually used PHP since I tend to work with Kirby CMS pretty often.
The function in PHP:
function max_img_width($img_width, $img_height, $ideal_area) {
$max_width = round($img_width * sqrt($ideal_area / ($img_width * $img_height)));
echo $max_width;
}
And the function in use + corresponding HTML:
<img style="max-width: <?php max_img_width(1500, 3000, 40000); ?>px;" src="https://piperhaywood.com/my-image.jpg">
A CSS-only solution that works now
There seems to be a CSS-only way to go about this though. Apparently you can approximate square roots in CSS by using a series of CSS variables and calc()
, see more info in this thread.
I will warn you: this is ugly. Also, it heavily relies on CSS variables which may or may not work for you depending on your browser support requirements. Here’s the CSS and the image markup:
img {
--width: 0;
--height: 0;
--ideal-area: 40000;
--area: calc(var(--width) * var(--height));
--ratio: calc(var(--ideal-area) / var(--area));
--guess01: calc(calc(var(--ratio) + calc( var(--ratio) / var(--ratio))) / 2);
--guess02: calc(calc(var(--guess01) + calc( var(--ratio) / var(--guess01))) / 2);
--guess03: calc(calc(var(--guess02) + calc( var(--ratio) / var(--guess02))) / 2);
--guess04: calc(calc(var(--guess03) + calc( var(--ratio) / var(--guess03))) / 2);
--guess05: calc(calc(var(--guess04) + calc( var(--ratio) / var(--guess04))) / 2);
--guess06: calc(calc(var(--guess05) + calc( var(--ratio) / var(--guess05))) / 2);
--guess07: calc(calc(var(--guess06) + calc( var(--ratio) / var(--guess06))) / 2);
--guess08: calc(calc(var(--guess07) + calc( var(--ratio) / var(--guess07))) / 2);
max-width: calc(var(--width) * var(--guess08) / 2 * 1px);
}
<img style="--width: 1500; --height: 3000;" src="https://piperhaywood.com/my-image.jpg">
I’d want to do some more browser testing since this results in a pretty gnarly calc()
situation by --guess08
, but at first glance this seems like it might be a worthwhile solution. It doesn’t give us exactly proportionate surface areas but it gets very close. It only starts to fall apart with super skyscraper-y and letterbox-y images.
A few quick notes regarding why this is written as it is and ways that it could be tweaked:
I capped the number of guesses at eight because any more seemed to just fail, Chrome and Safari wouldn’t interpret such a big calc()
equation.
I set the default width and height to zero so that there is no max width restriction if the image tag is missing the width and height CSS variables. This could be changed, as could the ideal area variable (increase to get larger images, decrease to get smaller).
The 1px
value in the max-width
calculation is required so that the value is interpreted as a unit. That said, it doesn’t have to be pixels! Could change this to another unit like 1em
or 1%
.
If I wanted to display these evenly centred, I’d probably give the images some margin and then wrap them in a container with styles as below:
.container {
align-items: center;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
Could also use CSS grid for a more consistent spacing result.
And a final reminder: this only visually scales the images. Try to avoid loading a 3000px wide image if you’re going to be displaying it around 200px wide, your users and the planet will thank you.
A CSS-only solution that might work in the future
So apparently more complex math functions including sqrt()
might be coming to CSS in the future! See this issue raised by Lea Verou in the CSS specifications editor’s drafts repo and the exponential functions section from CSS Values and Units Module Level 4 in the W3C editor’s draft from 3 February 2020 (a couple days ago!).
I’m not sure when this would become part of the spec and no idea if / when the browsers might implement them, but here is a snippet that should work with that new function in theory:
img {
--width: 0;
--height: 0;
--ideal-area: 40000;
max-width: calc(var(--width) * sqrt(calc(var(--ideal-area) / calc(var(--width) * var(--height)))) / 2 * 1px);
}
I’m happy that Nick asked the question on Twitter, I actually need this on an upcoming project where I’ve only got Twig to work with which doesn’t support square roots, and I’d prefer to avoid JavaScript in this instance. Hopefully this is the solution, will update here if so.
A picture says a thousand words, so see Nick’s very nice demo of area-based image sizing with CSS to check out one possible outcome.
Edit 05.02.20
Edited a few words for clarity, added Nick’s demo, added future example incorporating a CSS-based sqrt()
function.
Thanks Sam Baldwin for bringing future math function support in CSS to my attention!
Edit 06.02.20
Simplified the PHP + HTML example.
The original PHP + HTML example used a --max-width
CSS variable instead of just applying a max-width
directly. I used a CSS variable because I thought that they were compliant with a strict Content Security Policy that includes the style-src
directive set to unsafe-inline
. That assumption was incorrect, though there does seem to be some discussion about the topic.
Thanks Lizzie Malcolm for questioning the CSS var usage in the PHP example!
Edit 18.02.20
Changed parenthesis-enclosed arithmetic so that each is enclosed in calc()
. Plain CSS seems to interpret arithmetic enclosed in parenthesis just fine, but SASS doesn’t seem to like it.