Mastering Image Ratios With object-fit
Dom Jay
November 26, 2025
import InlineCalloutCluster from '../../components/InlineCalloutCluster.astro';
import InlineCallout from '../../components/InlineCallout.astro';
import ObjectFitDemo1 from '../../components/demos/ObjectFitDemo1.astro';
import ObjectFitDemo2 from '../../components/demos/ObjectFitDemo2.astro';
import { Picture } from "astro:assets";
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.
How object-fit works
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 .
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, . This property controls which part of the image stays visible when cover crops it. The default is , which works for most images, but for a portrait photo where the subject's face is at the 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.
Two properties, not one
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:
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. 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 to serve an image that works with those dimensions, rather than downloading a 1200px original and letting 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.
The code aboves has width and height attributes on the 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.
reserves the space. fills it cleanly. Add , , and while you're there. That's the whole pattern. No need for a library, or build step, or a polyfill.
Discussion in the ATmosphere