Growing
Last updated:

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.

No constraints. Images load at their natural sizes - a tall portrait image gets a tall slot, a wide landscape gets a wide one. As each image arrives, it pushes everything below it down. The text you're reading moves. That's CLS.
CLS score:

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

Hit "Simulate page load" to see layout shifts.
Space reserved instantly. 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.
CLS score:

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.

Wide landscape image

18 Oct 2025 · Article

Square image - 1:1 ratio

object-fit: cover crops it to 16:9. No layout disruption at all.

Wide landscape image

15 Oct 2025 · Article

Wide landscape - 16:9 ratio

Loads first. Space was already there. Nothing moves.

Another tall image

12 Oct 2025 · Article

Another tall image - 3:4 ratio

The last to arrive. The grid hasn't moved once.

Layout shift log

Hit "Simulate page load" to confirm: no shifts detected.
/* 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;
}
CLS (Cumulative Layout Shift) is a Core Web Vitals metric. A score below 0.1 is considered good. Scores are simulated here based on the number and size of shifts detected. · web.dev/cls

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…

Profile photo cropped at center - the top of the head is cut off

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.

Test image:

The five values - click to explore

cover

cover

contain

contain

fill

fill

none

none

scale-down

scale-down

Detail

Preview

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-position preview
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.

Browser Support

Fully supported

Chrome 32
Edge 79
Firefox 36
Safari 10