← All writing
Craft · · 5 min

aspect-ratio

The percentage-padding hack we built the responsive web on, replaced by a single declaration.

CSS

Everybody who has embedded a video knows this by heart:

the hack, since about 2009
.embed {
  position: relative;
  padding-top: 56.25%;   /* 9 ÷ 16, and everybody has a comment like this */
  height: 0;
}
.embed iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
CSS

Nine lines, a magic number, an absolutely positioned child, and a wrapper element that exists purely to hold a number. It’s in every codebase I’ve ever opened. I have copied it into projects without thinking about it for something like seven years.

There’s a property now. It’s in one browser, properly, with the others on the way, and it does the whole thing:

aspect.css
.embed { aspect-ratio: 16 / 9; }
CSS

No wrapper, no absolute positioning, no percentage arithmetic, and the child can be a normal element in normal flow.

Why the hack worked at all

Worth a paragraph, because it’s a genuinely odd bit of CSS trivia and I think it’s been quietly holding up the entire responsive era.

Percentage values on padding and margin resolve against the width of the containing block. All of them, including padding-top and padding-bottom. Which looks like a mistake, or at least like something nobody would design on purpose, and it’s been in the spec since before I could type.

So padding-top: 56.25% means “56.25% of my own width,” which is the only way CSS has ever had of expressing a height in terms of a width. We took a quirk of the box model and built every responsive video, every card image, every map embed and every placeholder on top of it, for over a decade, with a comment next to it explaining the division.

I have some affection for it. It’s the sort of thing that made the web feel like something you could outwit.

The three ways it isn’t what you’d assume

It’s a hint about the box’s automatic size, not a command about its dimensions, and three things follow from that.

If both dimensions are already determined, it does nothing. The ratio applies when one axis is auto. Set an explicit width and an explicit height and the ratio is simply ignored, silently, which is the correct behavior and looks like a bug.

Content can push it out of shape. If the box has text in it that doesn’t fit, the box grows, because the alternative would be overflowing content. So aspect-ratio on a card with a heading in it will be honored until somebody writes a long heading. If you need it strictly, min-height: 0 gives the box permission to be smaller than its contents, and then you handle the overflow deliberately.

min-height beats it. Which is fine and consistent with how minimums work everywhere else, and it catches people because a min-height inherited from some utility class three files away is invisible when you’re staring at the ratio.

Shipping it this year

One browser has it properly today and the other two are close but not there, so this isn’t something I’d put in without a fallback yet.

Happily, the fallback is the thing it replaces, and the feature query makes the handover clean:

progressive.css
.embed {
  position: relative;
  padding-top: 56.25%;
}
.embed > * { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }

@supports (aspect-ratio: 16 / 9) {
  .embed { padding-top: 0; aspect-ratio: 16 / 9; }
  .embed > * { position: static; }
}
CSS

Which is more code than either version on its own, obviously, and it’s transitional, and in about 18 months you delete the top half and it’s one line forever.

I’d start using it now on personal work and put the fallback in on client work, and I’d write a note in the repo saying which browsers the top half is for, so that whoever finds it in 2024 knows it’s safe to bin.

One line, forever

That’s it, really. This is a small post about a small property and I mainly wanted to record the moment, because there’s something quite nice about a hack that’s been in every project you’ve ever worked on just going away.

There’s a version of this that’s a bit wistful about the ingenuity we all had to have. I don’t feel that at all, honestly. I’d hand back every clever thing I ever learned about percentage padding in exchange for a declaration that says what shape the box is 🙃

Read similar posts
6 min

Style queries

Five surface variants, four child elements, and 20 selectors that existed only to tell a child which parent it was in. Style queries delete the whole grid of them. That isn't quite the same as saying you needed them.

6 min

currentColor

I opened a button component with 11 color declarations in it, across four variants, and the correct number was one per variant, because six of those properties already default to the thing they were being set to.