PPK looks at aspect-ratio, a CSS property for layout that, for the most part, does exactly what you would think it does. It’s getting more interesting as it’s behind a flag in Firefox and Safari now, so we’ll have universal support pretty darn soon. I liked how he called it a “weak declaration” which I’m fairly sure isn’t an official term but a good way to think about it.
Because you’ve explicitly set the height and width, that is what will be respected. The aspect-ratio is weak in that it will never override a dimension that is set in any other way.
And it’s not just height and width, it could be max-height that takes effect, so maybe the element follows the aspect ratio sometimes, but will always respect a max-* value and break the aspect ratio if it has to.
It’s so weak that not only can other CSS break the aspect ratio, but content inside the element can break it as well. For example, if you’ve got a ton of text inside an element where the height is only constrained by aspect-ratio, it actually won’t be constrained; the content will expand the element.
I think this is all… good. It feels intuitive. It feels like good default behavior that prevents unwanted side effects. If you really need to force an aspect ratio on a box with content, the padding trick still works for that. This is just a much nicer syntax that replaces the safe ways of doing the padding trick.
PPK’s article gets into aspect-ratio behavior in flexbox and grid, which definitely has some gotchas. For example, if you are doing align-content: stretch;—that’s one of those things that can break an aspect ratio. Like he said: weak declaration.
I recently came across an interesting problem. I had to implement a grid of cards with a variable (user-set) aspect ratio that was stored in a --ratio custom property. Boxes with a certain aspect ratio are a classic problem in CSS and one that got easier to solve in recent years, especially since we got aspect-ratio, but the tricky part here was that each of the cards needed to have two conic gradients at opposite corners meeting along the diagonal. Something like this:
User set aspect ratio cards.
The challenge here is that, while it’s easy to make an abrupt change in a linear-gradient() along the diagonal of a variable aspect ratio box using for example a direction like to top left which changes with the aspect ratio, a conic-gradient() needs either an angle or a percentage representing how far it has gone around a full circle.
Check out this guide for a refresher on how conic gradients work.
The simple solution
The spec now includes trigonometric and inverse trigonometric functions, which could help us here — the angle of the diagonal with the vertical is the arctangent of the aspect ratio atan(var(--ratio)) (the left and top edges of the rectangle and the diagonal form a right triangle where the tangent of the angle formed by the diagonal with the vertical is the width over the height — precisely our aspect ratio).
The angle of the diagonal with the vertical (edge).
However, no browser currently implements trigonometric and inverse trigonometric functions, so the simple solution is just a future one and not one that would actually work anywhere today.
The JavaScript solution
We can of course compute the --angle in the JavaScript from the --ratio value.
let angle = Math.atan(1/ratio.split('/').map(c => +c.trim()).reduce((a, c) => c/a, 1));
document.body.style.setProperty('--angle', `${+(180*angle/Math.PI).toFixed(2)}deg`)
But what if using JavaScript won’t do? What if we really need a pure CSS solution? Well, it’s a bit hacky, but it can be done!
The hacky CSS solution
This is an idea I got from a peculiarity of SVG gradients that I honestly found very frustrating when I first encountered.
Let’s say we have a gradient with a sharp transition at 50% going from bottom to top since in CSS, that’s a gradient at a 0° angle. Now let’s say we have the same gradient in SVG and we change the angle of both gradients to the same value.
As it can be seen below, these two don’t give us the same result. While the CSS gradient really is at 45°, the SVG gradient rotated by the same 45° has that sharp transition between orange and red along the diagonal, even though our box isn’t square, so the diagonal isn’t at 45°!
This is because our SVG gradient gets drawn within a 1x1 square box, rotated by 45°, which puts the abrupt change from orange to red along the square diagonal. Then this square is stretched to fit the rectangle, which basically changes the diagonal angle.
Note that this SVG gradient distortion happens only if we don’t change the gradientUnits attribute of the linearGradient from its default value of objectBoundingBox to userSpaceOnUse.
Basic idea
We cannot use SVG here since it only has linear and radial gradients, but not conic ones. However, we can put our CSS conic gradients in a square box and use the 45° angle to make them meet along the diagonal:
aspect-ratio: 1/ 1;
width: 19em;
background:
/* below the diagonal */
conic-gradient(from 45deg at 0 100%,
#319197, #ff7a18, #af002d 45deg, transparent 0%),
/* above the diagonal */
conic-gradient(from calc(.5turn + 45deg) at 100% 0,
#ff7a18, #af002d, #319197 45deg);
Then we can stretch this square box using a scaling transform – the trick is that the ‘/’ in the 3/ 2 is a separator when used as an aspect-ratio value, but gets parsed as division inside a calc():
You can play with changing the value of --ratio in the editable code embed below to see that, this way, the two conic gradients always meet along the diagonal:
Note that this demo will only work in a browser that supportsaspect-ratio. This property is supported out of the box in Chrome 88+ (current version is 90), but Firefox still needs the layout.css.aspect-ratio.enabled flag to be set to true in about:config. And if you’re using Safari… well, I’m sorry!
Enabling the flag in Firefox.
Issues with this approach and how to get around them
Scaling the actual .card element would rarely be a good idea though. For my use case, the cards are on a grid and setting a directional scale on them messes up the layout (the grid cells are still square, even though we’ve scaled the .card elements in them). They also have text content which gets weirdly stretched by the scaley() function.
The solution is to give the actual cards the desired aspect-ratio and use an absolutely positioned ::before placed behind the text content (using z-index: -1) in order to create our background. This pseudo-element gets the width of its .card parent and is initially square. We also set the directional scaling and conic gradients from earlier on it. Note that since our absolutely positioned ::before is top-aligned with the top edge of its .card parent, we should also scale it relative to this edge as well (the transform-origin needs to have a value of 0 along the y axis, while the x axis value doesn’t matter and can be anything).
body {
--ratio: 3/ 2;
/* other layout and prettifying styles */
}
.card {
position: relative;
aspect-ratio: var(--ratio);
&::before {
position: absolute;
z-index: -1; /* place it behind text content */
aspect-ratio: 1/ 1; /* make card square */
width: 100%;
/* make it scale relative to the top edge it's aligned to */
transform-origin: 0 0;
/* give it desired aspect ratio with transforms */
transform: scaley(calc(1/(var(--ratio))));
/* set background */
background:
/* below the diagonal */
conic-gradient(from 45deg at 0 100%,
#319197, #af002d, #ff7a18 45deg, transparent 0%),
/* above the diagonal */
conic-gradient(from calc(.5turn + 45deg) at 100% 0,
#ff7a18, #af002d, #319197 45deg);
content: '';
}
}
Note that we’ve moved from CSS to SCSS in this example.
This is much better, as it can be seen in the embed below, which is also editable so you can play with the --ratio and see how everything adapts nicely as you change its value.
Padding problems
Since we haven’t set a padding on the card, the text may go all the way to the edge and even slightly out of bounds given it’s a bit slanted.
Lack of padding causing problems.
That shouldn’t be too difficult to fix, right? We just add a padding, right? Well, when we do that, we discover the layout breaks!
This is because the aspect-ratio we’ve set on our .card elements is that of the .card box specified by box-sizing. Since we haven’t explicitly set any box-sizing value, its current value is the default one, content-box. Adding a padding of the same value around this box gives us a padding-box of a different aspect ratio that doesn’t coincide with that of its ::before pseudo-element anymore.
In order to better understand this, let’s say our aspect-ratio is 4/ 1 and the width of the content-box is 16rem (256px). This means the height of the content-box is a quarter of this width, which computes to 4rem (64px). So the content-box is a 16rem×4rem (256px×64px) rectangle.
Now let’s say we add a padding of 1rem (16px) along every edge. The width of the padding-box is therefore 18rem (288px, as it can be seen in the animated GIF above) — computed as the width of the content-box, which is 16rem (256px) plus 1rem (16px) on the left and 1rem on the right from the padding. Similarly, the height of the padding-box is 6rem (96px) — computed as the height of the content-box, which is 4rem (64px), plus 1rem (16px) at the top and 1rem at the bottom from the padding).
This means the padding-box is a 18rem×6rem (288px×96px) rectangle and, since 18 = 3⋅6, it has a 3/ 1 aspect ratio which is different from the 4/ 1 value we’ve set for the aspect-ratio property! At the same time, the ::before pseudo-element has a width equal to that of its parent’s padding-box (which we’ve computed to be 18rem or 288px) and its aspect ratio (set by scaling) is still 4/ 1, so its visual height computes to 4.5rem (72px). This explains why the background created with this pseudo — scaled down vertically to a 18rem×4.5rem (288px×72px) rectangle — is now shorter than the actual card — a 18rem×6rem (288px×96px) rectangle now with the padding.
So, it looks like the solution is pretty straightforward — we need to set box-sizing to border-box to fix our problem as this applied the aspect-ratio on this box (identical to the padding-box when we don’t have a border).
Sure enough, this fixes things… but only in Firefox!
Showing the difference between Chromium (top) and Firefox (bottom).
The text should be middle-aligned vertically as we’ve given our .card elements a grid layout and set place-content: center on them. However, this doesn’t happen in Chromium browsers and it becomes a bit more obvious why when we take out this last declaration — somehow, the cell in the card’s grid gets the 3/ 1 aspect ratio too and overflows the card’s content-box:
Checking the card’s grid with vs. without place-content: center.
In the meantime, what we can do to get around this is remove the box-sizing, padding and place-content declarations from the .card element, move the text in a child element (or in the ::after pseudo if it’s just a one-liner and we’re lazy, though an actual child is the better idea if we want the text to stay selectable) and make that a grid with a padding.
.card {
/* same as before,
minus the box-sizing, place-content and padding declarations
the last two of which which we move on the child element */
&__content {
place-content: center;
padding: 1em
}
}
Rounded corners
Let’s say we also want our cards to have rounded corners. Since a directional transform like the scaley on the ::before pseudo-element that creates our backgroundalso distorts corner rounding, it results that the simplest way to achieve this is to set a border-radius on the actual .card element and cut out everything outside that rounding with overflow: hidden.
However, this becomes problematic if at some point we want some other descendant of our .card to be visible outside of it. So, what we’re going to do is set the border-radius directly on the ::before pseudo that creates the card background and reverse the directional scaling transform along the y axis on the y component of this border-radius:
$r: .5rem;
.card {
/* same as before */
&::before {
border-radius: #{$r}/ calc(#{$r}*var(--ratio));
transform: scaley(calc(1/(var(--ratio))));
/* same as before */
}
}
Final result
Putting it all together, here’s an interactive demo that allows changing the aspect ratio by dragging a slider – every time the slider value changes, the --ratio variable is updated:
Here’s the release video skipped ahead to the aspect-ratio support:
For those catching up:
An aspect ratio defines the proportion of an element’s dimensions. For example, a box with an aspect ratio of 1/1 is a perfect square. An aspect ratio of 3/1 is a wide rectangle. Many videos aim for a 16/9 aspect ratio.
Some elements, like images and iframes, have an intrinsic aspect ratio. That means if either the width or the height is declared, the other is automatically calculated in a way that maintains its proportion.
Non-replaced elements, like divs, don’t have an intrinsic aspect ratio. We’ve resorted to a padding hack to get the same sort of effect.
Support for an aspect-ratio property in CSS allows us to maintain the aspect ratio of non-replaced elements.
Jen was just tweetin’ about how the latest Safari Technical Preview has aspect-ratio. Looks like Chrome and Firefox both have it behind a flag, so with Safari joining the party, we’ll all have it soon.
Once we can rely on it, FitVids (which I use on literally every site I make in one form or another) can entirely go away in favor of a handful of CSS applied directly to the elements (usually videos-in-<iframe>s).
Oh hey! A brand new property that affects how a box is sized! That’s a big deal. There are lots of ways already to make an aspect-ratio sized box (and I’d say this custom properties based solution is the best), but none of them are particularly intuitive and certainly not as straightforward as declaring a single property.
So, with the impending arrival of aspect-ratio (MDN, and not to be confused with the media query version), I thought I’d take a look at how it works and try to wrap my mind around it.
Shout out to Una where I first saw this and boy howdy did it strike interest in folks. Here’s me playing around a little.
Just dropping aspect-ratio on an element alone will calculate a height based on the auto width.
Without setting a width, an element will still have a natural auto width. So the height can be calculated from the aspect ratio and the rendered width.
If the content breaks out of the aspect ratio, the element will still expand.
The aspect ratio becomes ignored in that situation, which is actually nice. That’s why the pseudo-element tactic for aspect ratios was popular, because it didn’t put us in dangerous data loss or awkward overlap territory when content got too much.
But if you want to constrain the height to the aspect ratio, you can by adding a min-height: 0;:
If the element has both a height and width, aspect-ratio is ignored.
The combination of an explicit height and width is “stronger” than the aspect ratio.
Factoring in min-* and max-*
There is always a little tension between width, min-width, and max-width (or the height versions). One of them always “wins.” It’s generally pretty intuitive.
If you set width: 100px; and min-width: 200px; then min-width will win. So, min-width is either ignored because you’re already over it, or wins. Same deal with max-width: if you set width: 100px; and max-width: 50px; then max-width will win. So, max-width is either ignored because you’re already under it, or wins.
It looks like that general intuitiveness carries on here: the min-* and max-* properties will either win or are irrelevant. And if they win, they break the aspect-ratio.
.el {
aspect-ratio: 1 / 4;
height: 500px;
/* Ignored, because width is calculated to be 125px */
/* min-width: 100px; */
/* Wins, making the aspect ratio 1 / 2 */
/* min-width: 250px; */
}
With value functions
Aspect ratios are always most useful in fluid situations, or anytime you essentially don’t know one of the dimensions ahead of time. But even when you don’t know, you’re often putting constraints on things. Say 50% wide is cool, but you only want it to shrink as far as 200px. You might do width: max(50%, 200px);. Or constrain on both sides with clamp(200px, 50%, 400px);.
But say you run into that minimum 200px, and then apply a min-width of 300px? The min-width wins. It’s still intuitive, but it gets brain-bending because of how many properties, functions, and values can be involved.
Maybe it’s helpful to think of aspect-ratio as the weakest way to size an element?
It will never beat any other sizing information out, but it will always do its sizing if there is no other information available for that dimension.
Let’s build a literal grid of squares, and we’ll put the logos of some magazines centered inside each square. I imagine plenty of you have had to build a logo grid before. You can probably already picture it: an area of a site that lists the donors, sponsors, or that is showing off all the big fancy companies that use some product. Putting the logos into squares is a decent way of handling it, as it forces some clean structure amongst logos that are all different sizes, aspect ratios, and visual weights, which can get finicky and look sloppy.
By “grid” I mean CSS grid. Setting up a grid of (flexible) squares is a little trick of its own. Then we’ll plop those logos in there in a way that keeps them sized and centered. At the end, we’ll look at one little oddity.
1) Grid markup
Have you ever tried this in an editor that supports Emmet?
That will give us not only flexible columns, but a flexible number of columns. Remember, we don’t really care how many columns there are — we’re just building a grid of logos to display.
If we give each of those grid item divs a background and a forced height, we’d see something like:
This screenshot is at a parent width of about 800px. The three columns seen here will increase and decrease to fill the space, thanks to that grid magic.
2) Making real squares
Rather than force the grid items to any particular height, let’s use an aspect-ratio trick. We’ll plop an empty pseudo-element in there with padding-bottom: 100%; meaning it will force the height to be at least as tall as it is wide.
If we temporarily hide the images, you’ll see the grid of rectangles has become a grid of perfect squares:
3) Overlapping the Images
But with the images in there, they grow a little oblong because the image sits in the pseudo-element.
We’ll need a way to sit them on top of one another. Usually, with aspect-ratio techniques, we reach for absolute positioning to place the in-child container to cover the now aspect-ratioed shape. But since we’re using grid already anyway, let’s use grid again to place the items into the same space:
That says to make the grid items of the main grid also into grid containers, with no explicit rows and columns, then to place both the pseudo-element and image onto the first row and column of that grid. This will force them to overlap them, making a nice grid of squares again.
4) Placing the images
Let’s plop a proper src in there for each image. If we make sure the images fill the space (and limit themselves) with width: 100%, we’ll see them along the top of the grid items:
Not terrible, but we would prefer to see them centered. Here’s one trick to do that. First, we’ll also make their height: 100%, which distorts them:
In the video embedded below, Jen Simmons explains how to improve image loading by using width and height attributes. The issue is that there’s a lot of jank when an image is first loaded because an img will naturally have a height of 0 before the image asset has been successfully downloaded by the browser. Then it needs to repaint the page after that which pushes all the content around. I’ve definitely seen this problem a lot on big news websites.
Anyway, Jen is recommending that we should add height and width attributes to images like so:
This is because Firefox & Chrome will now take those values into consideration and remove all the jank before the image has loaded, even when you override those values in CSS with a fluid width and thus unknown height. That means content will always stay in the same position, even if the image hasn’t loaded yet. In the past, I’ve worked on a bunch of projects where I’ve placed images lower down the page simply because I want to prevent this sort of jank. I reckon this fixes that problem quite nicely.
• centered vertically + horizontally • scales to fit the viewport w/ a margin around it • maintains an arbitrary aspect ratio • No JS
There's a video in that tweet if it helps you visualize the challenge. I saw Paul Bakaus blogging about this the other day, too, so it's a thing that comes up!
Mark Huot got fancy applying aspect ratios directly with width/height and creating the margins from subtracting from those dimensions:
Amelia Wattenberger's idea is to set both height/width and max-height/max-width with viewport units, and center it with the classic translate trick:
Brian Hart used vmin units for the aspect ratio sizing and centered it with flexbox:
Benoît Rouleau did the same but used calc() for the margins in a different unit.
Andrew really likes Jonathan Melville's approach. Most of it is in Tailwind classes so it's a smidge hard for me to understand as I'm not used to looking at code like that yet.
Andrew said he ultimately went with the vmin thing — although I see he's using calc() to subtract vmin units from each other which isn't really necessary unless, I guess, you wanna see the math.
I learned something about percentage-based (%) padding today that I had totally wrong in my head! I always thought that percentage padding was based on the element itself. So if an element is 1,000 pixels wide with padding-top: 50%, that padding is 500 pixels. It's weird having top padding based on width, but that's how it works — but only sorta. The 50% is based on the parent element's width, not itself. That's the part that confused me.
The only time I've ever messed with percentage padding is to do the aspect ratio boxes trick. The element is of fluid width, but you want to maintain an aspect ratio, hence the percentage padding trick. In that scenario, the width of the element is the width of the parent, so my incorrect mental model just never mattered.
The difference is moot when you have a block element that expands to fill the width of its parent completely, as in every example used in this article. But when you set any width on the element other than 100%, this misunderstanding will lead to baffling results.
They pointed to this article by Aayush Arora who claims that only 1% of developers knew this.
Say you wanted to put the CSS-Tricks website in an <iframe>. You'd do that like this:
<iframe src="https://css-tricks.com"></iframe>
Without any other styling, you'd get a rectangle that is 300x150 pixels in size. That's not even in the User Agent stylesheet, it's just some magical thing about iframes (and objects). That's almost certainly not what you want, so you'll often see width and height attributes right on the iframe itself (YouTube does this).
Those attributes are good to have. It's a start toward reserving some space for the iframe that is a lot closer to how it's going to end up. Remember, layout jank is bad. But we've got more work to do since those are fixed numbers, rather than a responsive-friendly setup.
The best trick for responsive iframes, for now, is making an aspect ratio box. First you need a parent element with relative positioning. The iframe is the child element inside it, which you apply absolute positioning to in order to fill the area. The tricky part is that the parent element becomes the perfect height by creating a pseudo-element to push it to that height based on the aspect ratio. The whole point of it is that pushing the element to the correct size is a nicer system than forcing a certain height. In the scenario where the content inside is taller than what the aspect ratio accounts for, it can still grow rather than overflow.
I'll just put a complete demo right here (that works for images too):
A web page isn't locked in stone just because it has rendered visually. Media assets, like images, can come in and cause the layout to shift based on their size, which typically isn't known in fluid layouts until they do render. Or fonts can load and reflow layout. Or XHRs can bring in more content to be placed onto the page. We're always doing what we can to prevent the layout from shifting around — that's what I mean by layout jank. It's awkward and nobody likes it. At best, it causes you to lose your place while reading; at worst, it can mean clicking on something you really didn't mean to.
While I was trying to wrap my head around the new Layout Instability API and chatting it out with friends, Eric Portis said something characteristically smart. Basically, layout jank is a problem and it's being fought on multiple fronts:
Fight the problem at the browser behavior level:Scroll anchoring is designed to do this as well. The fact that it's now enabled by default is a big deal. It's also still possible to control it with CSS if you need to, via overflow-anchor.
Measure the problem and the causes: The Layout Instability API is designed for this, presumably to enable tooling that monitors it.
Say you have an image you're using in an <img> that is 800x600 pixels. Will it actually display as 800px wide on your site? It's very likely that it will not. We tend to put images into flexible container elements, and the image inside is set to width: 100%;. So perhaps that image ends up as 721px wide, or 381px wide, or whatever. What doesn't change is that image's aspect ratio, lest you squish it awkwardly (ignoring the special use-case object-fit scenario).
The main point is that we don't know how much vertical space an image is going to occupy until that image loads. This is the cause of jank! Terrible jank! It's everywhere and it's awful.
There are ways to create aspect-ratio sized boxes in HTML/CSS today. None of the options are particularly elegant because they rely on the "hack" of setting a zero height and pushing the boxes height with padding. Wouldn't it be nicer to have a platform feature to help us here? The first crack at fixing this problem that I know about is an intrinsicsize attribute. Eric Portis wrote about how this works wonderfully in Jank-Free Image Loads.
We'd get this:
<img src="image.jpg" intrinsicsize="800x600" />
This is currently behind a flag in Chrome 71+, and it really does help solve this problem.
But...
The intrinsicsize property is brand new. It will only help on sites where the developers know about it and take the care to implement it. And it's tricky! Images tend to be sized arbitrarily, and the intrinsicsize attribute will need to be custom on every image to be accurate and do its job. That is, if it makes it out of standards at all.
There is another possibility! Eric also talked about the aspect-ratio property in CSS as a potential solution. It's also still just a draft spec. You might say, but how is this helpful? It needs to be just as bespoke as intrinsicsize does, meaning you'd have to do it as inline styles to be helpful. Maybe that's not so bad if it solves a big problem, but inline styles are such a pain to override and it seems like the HTML attribute approach is more inline with the spirit of images. Think of how srcset is a hint to browsers for what images are available to download, allowing the browser to pick the best one for the scenario. Telling the browser about the aspect-ratio upfront is similarly useful.
I heard from Jen Simmons about an absolutely fantastic way to handle this: put a default aspect ratio into the UA stylesheet based on the elements existing width and height attributes. Like this:
It automatically has the correct aspect ratio as the page loads. That's awesome.
It's easy to understand.
A ton of the internet already has these attributes sitting on their images.
New developers will have no trouble understanding this, and old developers will be grateful there is little (if any) work to do here.
I like the idea of the CSS feature. But I like 100 times more the idea of putting it into the UAUA stylesheet is no small thing to consider — and I'm not qualified to understand all the implications of that — but it feels like a very awesome thing at first consideration.