Fix Images Without Dimensions for Better CLS

Images without width and height cause layout shifts. Add explicit dimensions to prevent your page from jumping around as images load.
Harlan WiltonHarlan Wilton8 min read Published

Images without explicit width and height cause 66% of CLS issues on mobile. Every image missing dimensions is a layout shift waiting to happen.

CLSCumulative Layout Shift CWV
25% weight
Good ≤0.10Poor >0.25

What's the Problem?

When you load an image without telling the browser its size, the browser reserves zero space for it. The image downloads, the browser discovers its actual dimensions, and everything below it gets shoved down. That's a layout shift.

This happens because browsers render content progressively. They don't wait for images to download before showing text. So they start laying out the page with a 0x0 placeholder for your image. When the image finally loads, the placeholder expands to fit it, and content reflows.

The technical flow:

  1. Browser parses HTML, finds <img> with no dimensions
  2. Creates 0x0 inline box for the image
  3. Renders page - user starts reading
  4. Image downloads, actual size determined (say, 800x600)
  5. Browser recalculates layout, content below shifts by 600px
  6. User loses their place, might misclick

The larger the image and the higher it appears on the page, the worse the shift. A hero image without dimensions can cause CLS of 0.3+ on its own - three times over the "poor" threshold.

How to Identify This Issue

Chrome DevTools

Open Elements panel and inspect your images. Look for:

  • Missing width attribute
  • Missing height attribute
  • CSS that sets width: auto or height: auto without an aspect-ratio

Quick console check:

document.querySelectorAll('img').forEach((img) => {
  const hasWidth = img.hasAttribute('width') || img.style.width
  const hasHeight = img.hasAttribute('height') || img.style.height
  const hasAspectRatio = getComputedStyle(img).aspectRatio !== 'auto'

  if (!(hasWidth && hasHeight) && !hasAspectRatio) {
    console.warn('Unsized image:', img.src, img)
  }
})

Lighthouse

The audit "Image elements do not have explicit width and height" lists every offending image with its URL and DOM location. Each image in that list is causing or could cause a layout shift.

Lighthouse checks that images have either:

  • Both width and height HTML attributes with valid non-negative integers
  • CSS width and height that aren't auto/initial/unset/inherit
  • One explicit dimension plus an explicit aspect-ratio

Images with position: fixed or position: absolute are excluded since they don't affect document flow.

The Fix

1. Add Width and Height Attributes

The simplest fix. Add width and height to every <img> element:

<!-- Before: CLS disaster -->
<img src="/hero.jpg" alt="Hero image">

<!-- After: No CLS -->
<img src="/hero.jpg" width="1200" height="630" alt="Hero image">

These attributes tell the browser the image's intrinsic dimensions. Modern browsers use them to calculate the aspect ratio and reserve the correct space before the image loads.

For responsive images, combine attributes with CSS:

<img
  src="/hero.jpg"
  width="1200"
  height="630"
  class="w-full h-auto"
  alt="Hero image"
>
.w-full { width: 100%; }
.h-auto { height: auto; }

The browser calculates: "This image has a 1200:630 (1.9:1) aspect ratio. At 100% width, I'll reserve height accordingly." No shift.

2. Use CSS aspect-ratio

When you can't add HTML attributes (CMS limitations, dynamic images), use CSS:

.hero-image {
  aspect-ratio: 16 / 9;
  width: 100%;
}

.avatar {
  aspect-ratio: 1;
  width: 48px;
}

.product-image {
  aspect-ratio: 4 / 3;
  width: 100%;
  object-fit: cover;
}

aspect-ratio tells the browser the proportions directly. Combined with one dimension (width or height), it calculates the other.

Fallback for older browsers:

.hero-image {
  aspect-ratio: 16 / 9;
  width: 100%;
}

/* Fallback using padding trick */
@supports not (aspect-ratio: 16 / 9) {
  .hero-image-wrapper {
    position: relative;
    padding-bottom: 56.25%; /* 9/16 = 0.5625 */
  }
  .hero-image-wrapper img {
    position: absolute;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}

3. Handle User-Uploaded Images

When dimensions are truly unknown (user uploads, external URLs), use container constraints:

<div class="image-container">
  <img src="/user-upload.jpg" alt="User content">
</div>
.image-container {
  aspect-ratio: 1; /* Force square */
  width: 100%;
  overflow: hidden;
  background: var(--ui-bg-muted);
}

.image-container img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

The container reserves space. The image fills it. Content around it never shifts.

For galleries with mixed aspect ratios, pick a dominant ratio:

.gallery-item {
  aspect-ratio: 4 / 3; /* Most photos are roughly this */
  background: var(--ui-bg-muted);
}

.gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: contain; /* or cover, depending on preference */
}

Why This Works

Browser aspect ratio calculation happens during the layout phase, before paint. When you provide dimensions (via attributes or CSS), the browser knows exactly how much space to reserve during initial layout.

The key insight: you're not preventing the image from loading - you're preventing the layout from changing when it does.

Framework-Specific Solutions

Next.jsnext/image requires dimensions and prevents CLS by design:
import Image from 'next/image'

// Static import - dimensions inferred
import heroImage from './hero.jpg'
<Image src={heroImage} alt="Hero" />

// External URL - dimensions required
<Image
  src="https://example.com/photo.jpg"
  width={800}
  height={600}
  alt="Photo"
/>

// Fill mode - container controls size
<div className="relative w-full aspect-video">
  <Image src="/hero.jpg" fill alt="Hero" />
</div>
The component generates srcset and handles responsive sizing. Using raw <img> in Next.js usually means missing out on these protections.
NuxtNuxtImg from @nuxt/image handles dimensions:
<template>
  <NuxtImg
    src="/photo.jpg"
    width="800"
    height="600"
    alt="Photo"
  />
</template>
With responsive sizes:
<NuxtImg
  src="/hero.jpg"
  width="1200"
  height="630"
  sizes="sm:100vw md:50vw lg:800px"
  alt="Hero"
/>
For dynamic images, wrap in a sized container:
<div class="aspect-video w-full">
  <NuxtImg
    :src="dynamicUrl"
    class="w-full h-full object-cover"
    alt="Dynamic content"
  />
</div>

Verify the Fix

After adding dimensions:

1. Re-run Lighthouse

The "Image elements do not have explicit width and height" audit should pass. If images still appear, they're missing proper sizing.

2. Check with DevTools

// Should return empty array
document.querySelectorAll('img').filter((img) => {
  const style = getComputedStyle(img)
  const hasExplicitSize
    = (img.width && img.height)
      || (style.width !== 'auto' && style.height !== 'auto')
      || style.aspectRatio !== 'auto'
  return !hasExplicitSize
})

3. Visual test

Throttle network to Slow 3G, reload the page. Watch the image areas. They should have placeholder space that fills in when images load - no jumping.

Expected improvement: Fixing a single unsized hero image can reduce CLS from 0.25+ to under 0.1. Multiple unsized images compound, so fixing all of them often results in CLS dropping to near zero from this source.

Common Mistakes

Only adding width

<!-- Wrong - height is still auto -->
<img src="/photo.jpg" width="800">

<!-- Right -->
<img src="/photo.jpg" width="800" height="600">

Both dimensions are required for aspect ratio calculation.

Wrong aspect ratio

<!-- Image is actually 1200x800, not 1200x600 -->
<img src="/photo.jpg" width="1200" height="600">

This causes the image to stretch or compress. Use actual image dimensions or matching aspect ratio.

Forgetting lazy-loaded images

<!-- Still needs dimensions even with lazy loading -->
<img src="/photo.jpg" loading="lazy" width="800" height="600">

Lazy loading defers download, not space reservation. Dimensions are still critical.

Using percentage for both dimensions

/* Doesn't help - browser still doesn't know absolute size */
img {
  width: 100%;
  height: 100%;
}

The parent needs a defined size, or use aspect-ratio with one dimension.

Unsized images often appear alongside:

Test Your Entire Site

One page might pass. Another might have a template with unsized images that repeats across hundreds of URLs. Unlighthouse scans your entire site and surfaces every page with unsized image issues, sorted by CLS impact.