I had a little situation the other day where I needed to make one of those aspect-ratio friendly boxes. This isn’t particularly new stuff. I think the original credit goes as far back as 2009 and Thierry Koblentz’s Intrinsic Ratios and maintained popularity even for other kinds of content with articles like Uncle Dave’s Ol’ Padded Box.
Let’s go on a little journey through this concept, as there is plenty to talk about.
The Core Concept: padding in percentages is based on width
Even when that is a little unintuitive, like for vertical padding. This isn’t a hack, but it is weird: padding-top
and padding-bottom
is based on the parent element’s width
. So if you had an element that is 500px wide, and padding-top
of 100%, the padding-top
would be 500px.
Isn’t that a perfect square, 500px × 500px? Yes, it is! An aspect ratio!
If we force the height of the element to zero (height: 0;
) and don’t have any borders. Then padding will be the only part of the box model affecting the height, and we’ll have our square.
Now imagine instead of 100% top padding, we used 56.25%. That happens to be a perfect 16:9 ratio! (9 / 16 = 0.5625).
Now we have a friendly aspect ratio box, that works well in fluid width environments. If the width changes, so does the height, and the element keeps that aspect ratio.
Use case: a background-image
Perhaps we’ve made a typographic lockup. It’s for the title of an article, so it makes sense to use an <h1>
tag.
<h1>
Happy Birthday
</h1>
We can make that <h1>
tag the aspect ratio box and apply the lockup as a background image.
h1 {
overflow: hidden;
height: 0;
padding-top: 56.25%;
background: url(/images/happy-birthday.svg);
}
But I lied about that aspect ratio up there. It’s not actually a 16:9 image. I just downloaded that graphic off a stock photography site. It happens to be an SVG with a viewBox="0 0 1127.34 591.44"
which means it’s essentially an 1127.34 × 591.44 image in terms of aspect ratio. Or it could have been a 328 × 791 image.
I’d say it’s very common for any random image not to fit into a very specific pre-defined aspect ratio…
The Math of Any Possible Aspect Ratio
Perfect squares and 16:9 stuff is great, but the values used for those are just simple math. An aspect ratio can be anything, and they commonly are completely arbitrary. A video or image can be cropped to any size.
So how do we figure out the padding-top for our 1127.34 × 591.44 SVG above?
One way is using calc()
, like this:
padding-top: calc(591.44 / 1127.34 * 100%);
It was expressed to me not long ago that using calc()
here may be “slower”, but I’ve never seen any evidence of that. I imagine that yes, the computer does need to calculate something, so in a head-to-head battle against a situation where it doesn’t, calculating is slower. But a math problem doesn’t seem like too much work for a computer. For example, the popular Intel Pentium III (released in 1999) could do 2,054 MIPS or “Millions of instructions per second”, so it would make imperceptively quick work of a division problem. And now chips are 50× faster.
If we were using a preprocessor like Sass, we could do the calculation ahead of time:
padding-top: 591.44px / 1127.34px * 100%;
Either way, I’m a fan of leaving the math in the authored code.
How do you put content inside if padding is pushing everything down?
We hid the content in the demo above, by letting the content get pushed out and hiding the overflow. But what if you need an aspect ratio box while keeping content inside of it? That’s slightly trickier. We’ll need to position it back up into place. Absolute positioning can be up for that job.
Say we’re just using text alone now, but still want the aspect ratio box. We’ll need an inside wrapper for the absolute positioning. Let’s get specific with our classnames:
<h1 class="aspect-ratio-box">
<div class="aspect-ratio-box-inside">
Happy Birthday
</div>
</h1>
Then do the positioning:
.aspect-ratio-box {
height: 0;
overflow: hidden;
padding-top: 591.44px / 1127.34px * 100%;
background: white;
position: relative;
}
.aspect-ratio-box-inside {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
Just for fun, let’s go full blast here and center that text and size it so it scales with the box:
<h1 class="aspect-ratio-box">
<div class="aspect-ratio-box-inside">
<div class="flexbox-centering">
<div class="viewport-sizing">
Happy Birthday
</div>
</div>
</div>
</h1>
Few more classes to style:
.flexbox-centering {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.viewport-sizing {
font-size: 5vw;
}
Use Case: Video
This is probably the #1 most common and practical use case for all this aspect ratio box stuff. HTML5 <video>
isn’t an issue, as it behaves as if it has an aspect ratio just like <img>
s do. But a lot of video on the web arrives in <iframe>
s, which do not scale with an aspect ratio.
This is exactly what FitVids is all about. It finds videos on the page, figures out their unique aspect ratios, then applies these same CSS concepts to them to make them fluid width while maintaining their unique aspect ratios.
FitVids is jQuery based, but there are vanilla JavaScript options, like this one by Ross Zurowski.
But… what if there is too much content?
Back to arbitrary (probably textual) content for a bit.
We’re essentially setting heights here, which should always flash a blinking red light when working with CSS. The web likes to grow downward, and setting fixed heights is an enemy to that natural movement.
If the content becomes too much for the space, we’re in Bad Design territory:
Maybe we could do something like overflow: auto;
but that might be Awkward Design Territory.
The Pseudo Element Tactic
This is what has become, I think, the best way to handle an aspect ratio box that has completely arbitrary content in it. Keith Grant has a good writeup on it. Marc Hinse had an explanation and demos back in 2013.
With this technique, you get the aspect ratio box with less markup, and it’s still safe if the content exceeds the height.
The trick is to apply the % padding to a pseudo-element instead of the box itself. You float the pseudo-element, so the content inside can be inside nicely, then clear the float.
.aspect-ratio-box {
background: white;
}
.aspect-ratio-box::before {
content: "";
width: 1px;
margin-left: -1px;
float: left;
height: 0;
padding-top: 591.44px / 1127.34px * 100%;
}
.aspect-ratio-box::after { /* to clear float */
content: "";
display: table;
clear: both;
}
See how it’s safer:
And a video:
Using Custom Properties
This is perhaps the coolest idea of all!
Thierry Koblentz recently wrote this up (Removing link as Thierry’s site is dead.), crediting Sérgio Gomes for the idea.
To use it, you set a custom property scoped right to the element you need it on:
<div style="--aspect-ratio:815/419;">
</div>
<div style="--aspect-ratio:16/9;">
</div>
<!-- even single value -->
<div style="--aspect-ratio:1.4;">
</div>
The CSS that styles this is gosh-danged genius:
[style*="--aspect-ratio"] > :first-child {
width: 100%;
}
[style*="--aspect-ratio"] > img {
height: auto;
}
@supports (--custom:property) {
[style*="--aspect-ratio"] {
position: relative;
}
[style*="--aspect-ratio"]::before {
content: "";
display: block;
padding-bottom: calc(100% / (var(--aspect-ratio)));
}
[style*="--aspect-ratio"] > :first-child {
position: absolute;
top: 0;
left: 0;
height: 100%;
}
Allow me to quote Thierry’s step-by-step explanation:
- We use
[style*="--aspect-ratio"]
as a hook to target the appropriate boxes - We stretch the inner box regardless of support for custom property
- We make sure the height of images comes from their intrinsic ratio rather than their
height
attribute - We style the container as a containing block (so the inner box references that ancestor for its positioning)
- We create a pseudo-element to be used with the “padding hack” (it is that element that creates the aspect ratio)
- We use
calc()
andvar()
to calculate padding based on the value of the custom property - We style the inner box so it matches the dimensions of its containing block
Other Ideas & Tools
Lisi Linhart tipped me off to Ratio Buddy, which is super cool:
Notice that it uses a pseudo-element, but then still absolutely positions the inside container. That’s kinda weird. You’d probably either skip the pseudo-element and put the padding directly on the container, or float the pseudo-element so you don’t need that inner container. Still, I like the idea of a little generator for this.
Tommy Hodgins has CSSplus which features Aspecty which is just for this, assuming you’re cool with a JavaScript parsing and changing your CSS kinda thing:
I’ve actually seen quite a bit of real world usage of aspect ratio boxes over the years. Feel free to share if you’ve had some experience!
I am thinking that if we could use
attr
insidecalc
asnumber
we’d use something like this:height: calc(attr(width)/calc(height) * ...)
Then you would have to resort to ancient width and height attributes on each element.
I like this idea. Yea, using height/width is a bit outdated now, but this makes it completely automated and works for things like pasted YouTube embeds, with no effort from a user (in the case of a CMS).
Truly a CSS Trick.
I have to wonder if this is something browsers will eventually come up with a proper solution for. The way it used to be standard practice to use
float:right
when you wanted to right-justify something next to fluid content, which has now been made unnecessary by flexbox.Nice thing with custom properties, my only doubt is that a selector like
[style*="--aspect-ratio"]
seems a little “fragile” to me. Some naming convention must be strictly held, or an attribute likestyle="--aspect-ratio-alt: 1.5"
would trigger the declaration.BTW, is it just me or we can’t see most of the videos? I have four “Failed to load resource: net::ERR_SPDY_PROTOCOL_ERROR” errors in the console (Chrome 60 on Mac – but also Chrome 58 on Android).
Fair point. But just to point out an extra-coolness of the technique,
style="--aspect-ratio-alt: 1.5"
still works!“padding-top and padding-bottom is based on an element’s width.”
I would amend this to read ‘based on the parent element’s width.’ It’s a small distinction, but took me forever to understand when I was first trying to figure this out. You can’t just create a div that’s 500px wide, set it to zero height and drop 100% padding on it and get that 500px red box. It’s got to be inside a 500px wide div to begin with!
And since we’re talking about it, this gets my vote for weirdest CSS behavior. I personally don’t understand how this came to be… but I am thankful it works! I use it in just about every project.
And the fact that it is based on a parent is why
is not weird at all.
Putting the padding on the pseudo element effectively makes the height ratio dependent on the width of the element itself, instead of on the width of the element’s container.
Putting the padding on the element itself would require fine-tuning the padding setting when you give the element a distinct width that is not 100%.
Thanks for that clarification. And I found it interesting that you use this in almost every project. I used it on one, and yeah, this is one of strangest aspect of CSS.
Another variant of the “pseudo-element” technique as outlined above is to use a flexbox container rather than theblock > float-and–clear pattern as shown, since flex row siblings will always be as tall as the tallest one in the row. Markup and style are even simpler:
flexbox max-aspect 2:1
Good compilation of traditional techniques or “CSS old school” ;-)
However today it is already possible to do the same without needing to calculate paddings, or declare “position: absolute” or have to use variables or other methods to maintain the aspect ratio.
All of this can be done by CSS Grid Layout for us because it is possible to place several elements in the same grid-area without leaving the document flow.
A pen as example:
https://codepen.io/Kseso/full/VWaWZR/
A post (sorry it´s on my blog) with some more examples and an explanation in Spanish.
Where I work, we have content producers requesting that ALL images are 16:9 ALL the time – which is a challenging ask when we’re also building customizable, responsive & mobile friendly websites.
Our solution was to support near-16:9 at the most common browser size, and work with our customers to explain the benefits of not locking content to a ratio all the time. We employ background-size/object-fit: contain as a stopgap if they still want it. It’s not worth us forming our whole platform around a single ratio.
Relevant recent related article:
https://www.bram.us/2017/06/16/aspect-ratios-in-css-are-a-hack/
Great pseudo-element tip for ratios! But how can you center text horizontaly, then? (Apply the float technique to the Happy Birhday demo.)
A flex solution break the % padding, absolute do not elarge the box-height…
Geoff Yuen writes in:
And points to Stack Overflow for an explanation: https://stackoverflow.com/questions/36783190/why-doesnt-percentage-padding-margin-work-on-flex-items-in-firefox-and-edge/36783414#36783414