Using aspect-ratio for Responsive Media and Preventing Layout Shift
Media that loads after the page renders is the classic cause of cumulative layout shift: the browser does not know how tall an image or video will be until its dimensions arrive, so content below it jumps when the box suddenly grows. The CSS aspect-ratio property fixes this by reserving the correct height from a known width and a declared ratio, before a single byte of the media loads. This guide covers aspect-ratio for responsive images, video, and embeds, and the padding-top hack fallback for older engines. It is part of Intrinsic Sizing Techniques within Mastering Container Queries & Responsive Layouts.
Problem statement
The narrow scenario is this: an article column contains a hero image, a <video> element, and a map <iframe>, all of which are fetched over the network after the HTML has already been laid out. On first paint the browser has no dimensions for any of them, so each collapses to a zero-height or intrinsic-default box. Seconds later the bytes arrive, the boxes expand to their real height, and every paragraph, heading, and button below them is shoved down the page. A reader who was mid-sentence loses their place; a reader who was reaching for a link taps whatever slid under their finger instead. The metric that captures this is Cumulative Layout Shift, and media is by far its most common cause. The fix has to be applied before the media is known, which rules out every technique that reacts to the load event.
Why a CSS ratio instead of fixed heights or JavaScript
The problem is precisely timing: layout happens before media dimensions are known. A fixed pixel height appears to solve it but breaks responsiveness — the media is locked to one height regardless of width, so it letterboxes or distorts as the column changes size. Reading the image's natural size in JavaScript and setting a height after load is worse, because that runs after the first layout, which is the exact moment the shift you wanted to prevent has already happened.
aspect-ratio resolves the timing problem inside CSS. Given a width — from a column, a grid track, or 100% of a parent — and a ratio, the engine computes the height during the first layout pass, so the box is the right size before the media arrives. When the media loads it pours into a box that already fits, and nothing below it moves. This is both a Core Web Vitals win (CLS approaches zero) and a correctness win, and it costs no script and no measurement.
There is an accessibility dimension as well. Reserving space keeps the page geometrically stable while assistive technology and keyboard focus traverse it; a user tabbing through a page does not have content reflow out from under them as images stream in. The ratio also respects zoom, because it is a pure proportion applied to whatever the zoomed width resolves to. A user at 200% browser zoom gets a box twice as wide and, automatically, twice as tall — the proportion is preserved without a single extra rule, which a hard-coded pixel height can never manage.
The one genuine tradeoff is that you must know the ratio at authoring time. For a design-system component whose slot always holds 16:9 promotional video, that is trivial. For user-generated uploads of arbitrary shape it is not, and there the honest answer is to emit the ratio server-side into an inline style attribute or a custom property, so the CSS still resolves the box on first layout. What you must not do is fall back to measuring in script, because that reintroduces the exact post-paint timing you were trying to escape.
Complete working implementation
The block below handles the three media cases — an image, a video element, and an iframe embed — each kept at a stable ratio with no layout shift, plus a @supports fallback to the padding hack for engines without the property.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 40rem; }
/* Reserve a 16:9 box from the width before media loads.
object-fit: cover crops to fill without distortion. */
.media-16x9 {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
display: block;
background: #7aa2ff14; /* visible placeholder while loading */
border-radius: 0.5rem;
}
/* A per-instance ratio, so one class serves several shapes.
The custom property defaults to 16/9 if nothing overrides it. */
.media-var {
aspect-ratio: var(--ratio, 16 / 9);
width: 100%;
object-fit: cover;
display: block;
}
/* iframes have no object-fit; the ratio alone shapes the box */
iframe.media-16x9 { border: 0; }
/* Fallback for engines without aspect-ratio: padding-hack wrapper.
padding-top: 56.25% == 9/16, reserving height as a % of width. */
@supports not (aspect-ratio: 16 / 9) {
.ratio-fallback {
position: relative;
width: 100%;
padding-top: 56.25%;
}
.ratio-fallback > * {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
}
</style>
</head>
<body>
<h2>Image</h2>
<!-- width/height attributes give browsers a default ratio too -->
<img class="media-16x9" width="1600" height="900" alt="Scenic ridge at dawn"
src="https://example.com/ridge.jpg">
<h2>Portrait image, ratio supplied per instance</h2>
<img class="media-var" style="--ratio: 3 / 4" width="900" height="1200"
alt="Standing stone" src="https://example.com/stone.jpg">
<h2>Video</h2>
<div class="ratio-fallback">
<video class="media-16x9" controls poster="https://example.com/poster.jpg">
<source src="https://example.com/clip.mp4" type="video/mp4">
</video>
</div>
<h2>Embed</h2>
<div class="ratio-fallback">
<iframe class="media-16x9" title="Location map"
src="https://example.com/map" loading="lazy"></iframe>
</div>
</body>
</html>
In the modern path the wrapper <div class="ratio-fallback"> is inert — the inner element sizes itself via aspect-ratio. Only when the property is unsupported does the @supports not rule activate the absolute-positioned fill. The --ratio custom property on the portrait image is the pattern to reach for when a template renders media of varying shape: the server knows the intrinsic dimensions, writes them into the inline style, and CSS still does all the sizing during first layout.
Key technique: ratio plus exactly one definite axis
aspect-ratio only reserves space when the box has a definite size on one axis to compute the other from. For block media that means giving the element width: 100% (or a track width) so the height resolves from the ratio. The property is aspect-ratio: <width> / <height>, a unitless proportion, so 16 / 9, 4 / 3, and 1 / 1 all work; a single number is equally valid, since aspect-ratio: 1.5 means the same as 3 / 2.
The failure mode worth internalising is the opposite of the one people expect. aspect-ratio is not ignored because the box is too small — it is ignored because the box is over-specified. If both axes are already definite, there is nothing left for the ratio to compute and it is silently discarded. That is why a flex item stretched by the default align-items: stretch often appears to lose its ratio: the cross axis is being sized by the flex container, so the height is definite before the ratio ever gets a say. Setting align-self: start (or height: auto) hands the height back to the ratio.
The same logic explains the interaction with content. The property maps to the preferred size, so real content taller than the computed height will push the box past it unless you also constrain overflow. The full aspect-ratio: auto 16 / 9 form spells out the priority explicitly: use the element's natural ratio when it has one, and fall back to 16:9 when it does not. For replaced elements with an intrinsic ratio, that is usually what you want. And note that object-fit: cover is doing separate work from the ratio — the ratio decides the box, object-fit decides how the pixels are fitted into it. Without cover, a 4:3 photograph poured into a 16:9 box is stretched.
Variation: a dark-mode placeholder and a container-driven ratio
While media streams in, the reserved box can show a theme-aware placeholder so the layout reads as intentional rather than blank, and it still never shifts. This pairs naturally with prefers-color-scheme. The second half of the example changes the ratio itself based on how much room the component has — a card in a narrow sidebar wants a squarer, taller image; the same card across a wide grid wants a cinematic strip. Because both branches are pure ratios, switching between them reserves the correct space in either state and never causes a shift.
.media-16x9 {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
background: #eef1f8; /* light placeholder */
}
@media (prefers-color-scheme: dark) {
.media-16x9 { background: #1c2230; } /* dark placeholder, same box */
}
/* The card is its own container, so the ratio can respond to the
component's width rather than the viewport's. */
.card { container-type: inline-size; container-name: card; }
/* Narrow card: a taller 4:3 crop reads better in a sidebar column. */
@container card (max-width: 380px) {
.media-16x9 { aspect-ratio: 4 / 3; }
}
/* Wide card: a cinematic banner across the top. */
@container card (min-width: 700px) {
.media-16x9 { aspect-ratio: 21 / 9; }
}
Because the box dimensions come from the ratio, changing the placeholder colour is purely cosmetic and cannot reintroduce layout shift, and swapping the ratio inside a @container block is resolved during layout just like the base rule. If you need the crop itself to move rather than just the box shape, add object-position alongside — for example object-position: 50% 30% in the narrow state to keep faces in frame when the box gets shorter. Naming the container, as covered in the guidance on nesting and naming container queries, matters here because media is often nested several levels inside a card and you want the query bound to a specific ancestor.
Browser support note
aspect-ratio is supported in Chrome 88+, Edge 88+, Firefox 89+, and Safari 15+, so it is reliable on every engine shipped since 2021. Browsers also derive a default ratio from an <img> element's width and height attributes, behaviour that landed in every engine before the CSS property did. That reserves space even with no CSS rule at all, which makes it the single highest-value line of markup for CLS on an image-heavy page. For anything older, the @supports not (aspect-ratio: 16 / 9) block falls back to the padding-top percentage hack; the mechanics of that gate are covered in the guide to feature detection with @supports. The container-query variant above needs Chrome and Edge 105+, Safari 16+, or Firefox 110+, and degrades to the base 16:9 rule everywhere else.
FAQ
How does aspect-ratio prevent cumulative layout shift? It reserves the correct box height from the ratio and the known width before the media loads, so when the image or video arrives it fills the existing box instead of pushing surrounding content down.
Do I still need width and height attributes on images?
Keep them. Modern browsers derive a default aspect-ratio from the width and height attributes automatically, which reserves space even without a CSS rule. The CSS aspect-ratio property is for elements without those attributes, like videos and iframes.
What is the padding-hack fallback for aspect-ratio?
Wrap the media in a container with padding-top set to the ratio as a percentage of width, and position the media absolutely to fill it. It reserves height the same way, for engines that predate the aspect-ratio property.
Does aspect-ratio work on iframes and video embeds?
Yes. aspect-ratio applies to any element with a definite width, including iframe and video. It is the cleanest way to make a YouTube or map embed responsive without the padding wrapper.
Why is my aspect-ratio being ignored on a flex or grid item?
Because the item has a definite size on both axes, or a stretched cross axis is supplying a height. Set align-self: start or give the item height: auto so only one axis is definite and the ratio can resolve the other.
Related
- Intrinsic Sizing Techniques — the parent guide for content-driven box sizing.
- min-content, max-content, fit-content() Explained — sizing boxes from their content rather than a ratio.
- Preventing Flex and Grid Overflow — what to do when a ratioed box refuses to shrink inside a track.
- Building Responsive Cards with Container Queries — cards whose media keeps a stable ratio across sizes.
- Optimizing CSS Animations for 60fps — cross-area: keeping rendering stable and shift-free under motion.
Related articles
More pages in the same section.