Skip to content

Guides

Responsive Images with srcset — Explained Properly

srcset and sizes without the confusion. How the browser actually picks an image, what w vs x descriptors do, and the patterns that work in production.

The responsive image syntax has been in HTML for almost a decade, and most developers still copy it from Stack Overflow without fully understanding what’s happening. That’s fine until something breaks — a wrong image size downloads, a hero renders fuzzy, or the browser picks a 2x asset on a 1x device. This guide covers what srcset and sizes actually do, when to use w versus x descriptors, and the patterns that hold up in production.

The problem responsive images solve

Your site has a hero image. On a 400px phone it renders at 400px wide. On a 4K desktop it renders at 1920px wide. On a Retina tablet it renders at 1024px CSS pixels but 2048px actual pixels.

Without responsive images, you ship one file. You can either ship a big file that the phone wastes bandwidth on, or a small file that the 4K desktop makes blurry. There’s no right answer.

srcset and sizes let you ship multiple versions and let the browser pick the right one per device.

Width descriptors (the w syntax)

The most common form:

<img
  src="/images/hero-800.jpg"
  srcset="/images/hero-400.jpg 400w,
          /images/hero-800.jpg 800w,
          /images/hero-1600.jpg 1600w,
          /images/hero-2400.jpg 2400w"
  sizes="(max-width: 600px) 100vw,
         (max-width: 1200px) 80vw,
         1200px"
  alt="Hero image"
  width="1200"
  height="600">

Three things are happening here:

  1. srcset lists candidates. Each entry says “here’s a file, and its actual pixel width is X.” The w suffix means “this file is X pixels wide.” It’s not a media condition — the browser is just being told the intrinsic width of each option.
  2. sizes tells the browser how big the image will be in CSS pixels at each breakpoint. “At max-width 600px, the image is 100vw; at max-width 1200px, it’s 80vw; otherwise it’s 1200px.”
  3. The browser calculates. It looks at the viewport width, figures out from sizes how many CSS pixels the image will occupy, multiplies by the device pixel ratio, and picks the smallest file from srcset that meets or exceeds that pixel count.

On a 390px iPhone with 3x pixel density: the image is 100vw = 390 CSS pixels. Actual pixels needed = 390 × 3 = 1170. The browser picks hero-1600.jpg (the smallest file ≥ 1170). On a 2560px desktop at 1x: the image is 1200 CSS pixels × 1 = 1200 actual pixels. The browser picks hero-1600.jpg again. On a 2560px desktop at 2x: 1200 × 2 = 2400 actual pixels. Picks hero-2400.jpg.

Density descriptors (the x syntax)

The other form:

<img
  src="/images/logo.png"
  srcset="/images/logo.png 1x,
          /images/logo@2x.png 2x,
          /images/logo@3x.png 3x"
  alt="Logo">

This is simpler and only works when the image is rendered at a single fixed CSS size regardless of viewport. Logos, icons, and UI elements with fixed dimensions are the canonical use case. 1x is your standard-density asset, 2x is double the pixels for Retina, 3x for the phones that want it.

You cannot mix w and x descriptors in the same srcset. Pick one per element.

When to use which

Use w descriptors when:

  • The image changes size with the viewport (most hero images, blog body images, product photos).
  • You want the browser to trade off between file size and resolution based on both viewport and density.
  • You care about bandwidth for mobile users.

Use x descriptors when:

  • The image has a fixed CSS size regardless of viewport (UI icons, logos, avatars, thumbnails in a grid where each cell is the same size).
  • You don’t need sizes because the CSS size is constant.
  • Simplicity matters more than perfect efficiency.

The mistake most people make is using x descriptors on responsive images. The browser can’t make good decisions if it doesn’t know how big the image will render.

The sizes attribute, demystified

sizes is a list of media conditions paired with image sizes, evaluated in order. The first matching condition wins. A final size with no condition is the default.

sizes="(max-width: 600px) 100vw,
       (max-width: 1200px) 80vw,
       1200px"

Reads as: “if the viewport is ≤600px, the image is 100vw wide. Otherwise, if the viewport is ≤1200px, it’s 80vw. Otherwise, it’s a fixed 1200px.”

Mistakes I’ve seen repeatedly:

  • sizes="100vw" on an image that’s never actually 100vw. You’re telling the browser to download the biggest file because you said the image fills the viewport. If your design caps the image at 800px, say so.
  • Forgetting sizes entirely. Without it, browsers assume sizes="100vw". Same problem.
  • Writing sizes backwards. Media conditions with min-width go largest to smallest. With max-width, smallest to largest. Getting the order wrong means the first condition always matches and later ones are dead.

The <picture> tag for format switching

srcset handles size variants. <picture> handles format variants.

<picture>
  <source type="image/avif"
          srcset="/img/hero-800.avif 800w, /img/hero-1600.avif 1600w"
          sizes="(max-width: 900px) 100vw, 1200px">
  <source type="image/webp"
          srcset="/img/hero-800.webp 800w, /img/hero-1600.webp 1600w"
          sizes="(max-width: 900px) 100vw, 1200px">
  <img src="/img/hero-1600.jpg"
       srcset="/img/hero-800.jpg 800w, /img/hero-1600.jpg 1600w"
       sizes="(max-width: 900px) 100vw, 1200px"
       alt="Hero"
       width="1600"
       height="800">
</picture>

The browser picks the first <source> whose type it supports. AVIF-capable browsers take the first, WebP-capable browsers (but not AVIF) take the second, and everything else falls through to the <img>.

You repeat sizes across the sources because each source has its own srcset. Yes, that’s verbose. A build-time image pipeline or a CMS plugin usually generates it.

How many variants do you need

The practical answer: fewer than you think. A common set:

VariantWidthUse case
Small400-600pxPhone, thumbnail
Medium800-1000pxPhone retina, tablet, small desktop
Large1400-1600pxDesktop, tablet retina
X-large2400pxDesktop retina, 4K

Four variants covers the vast majority of real devices. Going to 6 or 8 rarely moves real-user performance because browser selection rounds up to the next available candidate.

Generate them with Resize Image for one-offs, or Bulk Resize for a whole set. For social media specifically, Social Media Resizer handles the per-platform dimensions.

Don’t forget width and height

This comes up in every responsive-image article for good reason. On the <img> tag, width and height should be the intrinsic dimensions of the src file. Browsers use the ratio to reserve layout space before the image loads, which is what keeps Cumulative Layout Shift low.

<img src="/images/hero-800.jpg"
     srcset="..."
     sizes="..."
     width="800"
     height="400"
     alt="...">

You can still style the rendered size with CSS (max-width: 100%; height: auto;). The attributes are aspect-ratio hints, not fixed sizes.

Lazy loading and fetch priority

Two attributes that combine with srcset:

  • loading="lazy" defers loading until the image is near the viewport. Use it on below-the-fold images. Don’t use it on the LCP image — that delays LCP.
  • fetchpriority="high" tells the browser to prioritize this image. Use it on the LCP image. Don’t use it on everything — setting high priority on every image is the same as setting it on none.

A typical template:

<!-- LCP hero -->
<img src="/hero-1600.webp" srcset="..." sizes="..."
     width="1600" height="800"
     fetchpriority="high" decoding="async"
     alt="Hero">

<!-- Below-the-fold body image -->
<img src="/body-800.webp" srcset="..." sizes="..."
     width="800" height="600"
     loading="lazy" decoding="async"
     alt="Body image">

A realistic workflow

  1. Keep source images as large as you’ll ever need (2400-3000px wide for heroes).
  2. At build time, generate variants at 400w, 800w, 1600w, 2400w.
  3. For each variant, also generate WebP and ideally AVIF.
  4. In your templates, emit <picture> with the three format sources and the right sizes for the layout.
  5. Set width, height, and (on the LCP image) fetchpriority="high".
  6. Use loading="lazy" and decoding="async" on everything below the fold.

That’s it. The payoff is real: sites that ship this pattern typically see 30-50% less image bandwidth on mobile, visibly faster Largest Contentful Paint, and a Cumulative Layout Shift so low Google stops complaining. Done once, the pattern repeats across every page.

Related tools

Frequently asked questions

What's the difference between w and x descriptors in srcset?

w descriptors tell the browser the intrinsic pixel width of each candidate file (hero-800.jpg 800w means that file is 800 pixels wide), and combined with sizes the browser picks the best match for the viewport and device pixel density. x descriptors tell the browser the pixel density a file is designed for (logo@2x.png 2x for Retina), and only work when the image renders at a single fixed CSS size. Use w for images that scale with the viewport; use x for logos, icons, and fixed-size UI elements. You can't mix them in the same srcset.

Does srcset work without sizes?

Technically yes, but the browser falls back to assuming sizes=100vw — meaning the image fills the whole viewport — and will download the largest file in your srcset. That's usually not what you want. If the image is capped at 800 px by your layout, say so in sizes (sizes=800px) so the browser can pick a smaller file on narrow screens. Forgetting sizes is one of the most common reasons srcset doesn't save any bandwidth in practice.

Do I still need srcset if I use AVIF?

Yes. srcset handles size variants; picture with type handles format variants. A modern responsive image typically uses both: picture sources for AVIF and WebP, each with their own srcset of width variants, and an img fallback with a JPG srcset. AVIF alone gives you format efficiency but you're still shipping one size to everyone, wasting bandwidth on phones and blurring on 4K. Combine the two for the full win.

How many srcset variants do I actually need?

Fewer than you think — four widths (400w, 800w, 1600w, 2400w) covers the vast majority of real devices. Going to six or eight rarely moves real-user performance because browser selection rounds up to the next available candidate. For social and thumbnail contexts, fewer variants may be enough. Keep the set small, generate them at build time with a resize tool, and emit picture markup with AVIF plus WebP plus a JPG fallback for each variant.

Should I use loading=lazy and fetchpriority=high on the same image?

No — they're opposites. loading=lazy defers loading until the image approaches the viewport, which is what you want for below-the-fold images but disastrous for the LCP image. fetchpriority=high tells the browser to prioritize the image, which is what you want for the LCP image. Use fetchpriority=high plus decoding=async on the hero, and loading=lazy plus decoding=async on everything below the fold. Never put fetchpriority=high on everything — it defeats its own purpose.

Sponsored