Mastering Image Ratios With object-fit
Stop the jumps: the two-line CSS fix that kills CLS for good
The Silent Killer of Web Performance
Images are patient. They’ll wait until the last possible moment to load, and when they do, they’ll take exactly as much space as their natural dimensions require - regardless of what’s already on the page. For users, that means content jumping. For Core Web Vitals, it means a CLS hit. For you, it means a client bug ticket.
Two lines of CSS fix it. The demo below shows exactly what’s happening and why.
The Problem
This is the problem we’re trying to solve with Object-Fit. It’s quite prevalent on older sites, or ones that haven’t yet adopted effective responsive design practices. By refreshing the page, if it wasn’t noticeable on page load, the example below - our ‘bad behavior’ demo - will highlight exactly the issue that Object-Fit solves. These images have none of these practices, nor size attributes which means that not only do you have a decreased Cumulative Layout Shift (CLS) score, but a poor user experience. The card content will appear first at the top of the row, until the image loads, where it will then shift down to its expected position. Pretty nasty, right? Couple this with a slow internet connection, and it can become a real nightmare, and fast.
Images without fixed dimensions cause content to jump as they load - a Cumulative Layout Shift (CLS) failure. Hit reload on each panel to watch it happen, then see how two lines of CSS prevent it entirely.
Live demo
20 Oct 2025 · Article
Portrait image - 2:3 ratio
This content jumps when the tall image above it loads in.
18 Oct 2025 · Article
Square image - 1:1 ratio
A different height to its neighbour. The grid is already broken.
15 Oct 2025 · Article
Wide landscape - 16:9 ratio
Loads first because it's small. But the row height is wrong.
12 Oct 2025 · Article
Another tall image - 3:4 ratio
The last to arrive. Watch the grid shift one final time.
Layout shift log
aspect-ratio: 16 / 9 on the container tells the browser exactly
how tall each slot will be before any image arrives. object-fit: cover handles the cropping. Images load into pre-reserved space - no shift, no jump.
Live demo
20 Oct 2025 · Article
Portrait image - 2:3 ratio
Same tall image - but the slot is already 16:9. It loads into reserved space.
18 Oct 2025 · Article
Square image - 1:1 ratio
object-fit: cover crops it to 16:9. No layout disruption at all.
15 Oct 2025 · Article
Wide landscape - 16:9 ratio
Loads first. Space was already there. Nothing moves.
12 Oct 2025 · Article
Another tall image - 3:4 ratio
The last to arrive. The grid hasn't moved once.
Layout shift log
/* Reserve space before the image loads */
.card-img-wrap {
aspect-ratio: 16 / 9;
overflow: hidden;
}
/* Fill that space, crop neatly */
.card-img-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}How object-fit works
object-fit controls the relationship between an image’s natural dimensions and the space its container has reserved for it. Without it, the browser makes that decision for you, and almost always it will choose the wrong choice for cards, headers, and profile images. Overall, there’s five values you can use, but the most typically used one would be cover.
cover fills the container completely, cropping the image if the aspect ratios don’t match. The image is never distorted or forced to fit…it just fits cleanly, whatever the container dimensions are.
It’s even more beneficial when you use this alongsite its companion property, object-position. This property controls which part of the image stays visible when cover crops it. The default is center, which works for most images, but for a portrait photo where the subject’s face is at the top, object-position: top prevents the crop from cutting them out. Otherwise…
Fantastic.
Every object-fit value, on the same image
To really drill down into it, here’s a small demo that shows a few different image sources with different dimensions. Clicking any value will make it available to test how it behaves with portrait, landscape, and square content.
The five values - click to explore
cover
contain
fill
none
scale-down
Detail
object-position
object-position works alongside any value that clips the image with cover being the most common in production. The demo here uses object-fit: none so the full image is visible and the repositioning effect is obvious. With cover, the same coordinates apply, you'd just be moving the crop window rather than the whole image.
object-fit: none object-position: center object-fit works on any replaced element - img, video, iframe.
Two properties, not one
object-fit controls how an image fills its container. It doesn’t control how big that container is before the image loads - and that’s the gap that causes layout shift.
Without a fixed container size, the browser doesn’t know how much vertical space to reserve for an image slot. It allocates nothing, renders the surrounding content, and then reshuffles everything when the image arrives. That’s CLS - and object-fit alone can’t prevent it. The fix is aspect-ratio on the container:
.card-img-wrap {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.card-img-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
aspect-ratio tells the browser exactly how much space you should reserve for the image for when it arrives, so when it does load it has a container waiting for it and nothing on the page moves.
You might have seen an old classic at use for this: the padding hack, where a percentage value for the padding-top property is set on a container with position: relative to fake a fixed ratio. It worked, but it was nasty, and a nightmare to explain to the next developer. aspect-ratio replaced it entirely and has been supported in all browsers since September 2021.
The performance upside
Once the container has a fixed ratio, you know its exact display dimensions at every breakpoint. That’s not just useful for layout stability - it means you can serve precisely sized images. Using an aspect ratio of 16/9 within a container that’s 600px wide, you know that the display size is 600x338px, so you can then use srcset to serve an image that works with those dimensions, rather than downloading a 1200px original and letting object-fit: cover deal with the crop in the browser.
Without using a fixed ratio, there’s guesswork. Serving a large image and then hoping/praying that the browser crops it correctly, or at least to a somewhat passable degree.
<div class="card-img-wrap">
<picture>
<source
srcset="image-600x338.jpg 600w, image-1200x675.jpg 1200w"
sizes="(max-width: 768px) 100vw, 600px"
/>
<img
src="image-600x338.jpg"
alt="Descriptive alt text"
width="600"
height="338"
loading="lazy"
/>
</picture>
</div>
The code aboves has width and height attributes on the <img> element to help establish the aspect ratio as a hint to the browser before the CSS loads. The result is smaller file transfers, no layout shift, and consistent cropping across every card in the grid. That’s what the CLS demo at the top of this post is showing. It’s not just a visual fix, but a performance strategy as well.
Go fix your images
If you’ve got a card grid in production, open DevTools and run a Lighthouse audit. A CLS score above 0.1 on image-heavy pages is almost always this - unsized images loading into unsized containers. Two properties, five minutes, and you can wrap it up.
aspect-ratio reserves the space. object-fit: cover fills it cleanly. Add width, height, and loading="lazy" while you’re there. That’s the whole pattern. No need for a library, or build step, or a polyfill.