HTML

Responsive Images: srcset, sizes & the picture Element

responsive-images-guide

A few months ago I audited a portfolio site that was loading a 4MB hero image on mobile. The same 3840×2160 JPEG on a 375px wide iPhone screen. The phone was downloading 4 megabytes to display an image at one-tenth of its resolution. That's not a rounding error — it's a failure of basic image delivery.

Responsive images fix this. The HTML spec gives us srcset, sizes, and the <picture> element to serve the right file to the right device. But the syntax looks intimidating the first time you see it, so most developers just... don't use them. Let's change that.

The Problem: One Image Doesn't Fit All

Images are typically the heaviest assets on a web page. The HTTP Archive reports that images account for roughly 50% of a typical page's total weight. Serving oversized images wastes bandwidth, slows down page loads, and tanks your Core Web Vitals scores.

The solution is simple in concept: serve smaller images to smaller screens. But "smaller screens" is more nuanced than viewport width. A 13-inch MacBook Pro with a 2x Retina display needs a higher resolution image than a 15-inch budget laptop at 1x. Screen size and pixel density both matter.

srcset with Width Descriptors

The srcset attribute gives the browser a menu of image files to choose from. You describe what's available; the browser picks the best one based on the viewport and display density.

<img
  src="photo-800.jpg"
  srcset="
    photo-400.jpg 400w,
    photo-800.jpg 800w,
    photo-1200.jpg 1200w,
    photo-1600.jpg 1600w
  "
  sizes="(max-width: 600px) 100vw,
         (max-width: 1024px) 50vw,
         800px"
  alt="Mountain landscape at sunset"
>

Let's break this down because there's a lot happening in those few lines.

The srcset attribute lists your image files with width descriptors (the w suffix). photo-400.jpg 400w means "this file is 400 pixels wide." You're not telling the browser which one to use — you're telling it what's available.

The sizes attribute tells the browser how wide the image will be displayed at different viewport widths. It's a list of media conditions paired with lengths:

The browser combines sizes with the device pixel ratio to pick the right file. On a 375px wide phone with a 2x display, it calculates: 375px × 2 = 750px needed. It'll grab photo-800.jpg — the closest option that's large enough.

The src attribute is your fallback. Browsers that don't support srcset (essentially none in 2026, but still) will use it. Always include it, pointing to a sensible default — usually the medium-sized version.

srcset with Pixel Density Descriptors

There's a simpler version of srcset for images that are always displayed at the same CSS size, like logos or icons:

<img
  src="logo-1x.png"
  srcset="
    logo-1x.png 1x,
    logo-2x.png 2x,
    logo-3x.png 3x
  "
  alt="Company logo"
  width="200"
  height="60"
>

The 1x, 2x, 3x descriptors tell the browser which file to use based on the display's pixel density. A standard display gets the 1x version. A Retina display gets 2x. No sizes attribute needed because the display size doesn't change.

I use this for logos, icons, and any image with a fixed CSS width. It's simpler than width descriptors and works perfectly for that use case.

The picture Element: Art Direction

Here's where srcset falls short: it lets the browser choose the resolution, but the image content stays the same. What if you want to show a different crop on mobile? A wide landscape on desktop, but a tight portrait crop on phone?

That's art direction, and it requires the <picture> element:

<picture>
  <source
    media="(max-width: 600px)"
    srcset="hero-mobile.jpg"
  >
  <source
    media="(max-width: 1024px)"
    srcset="hero-tablet.jpg"
  >
  <img
    src="hero-desktop.jpg"
    alt="Team collaboration workspace"
  >
</picture>

With <picture>, you control which image loads at which breakpoint. The browser uses the first <source> whose media query matches. If none match, it falls back to the <img> element.

The critical difference: srcset with sizes is a hint — the browser has final say. <picture> with <source> elements is a directive — the browser must use the matching source.

Format Selection with picture

The other major use case for <picture> is serving modern image formats with fallbacks:

<picture>
  <source type="image/avif" srcset="photo.avif">
  <source type="image/webp" srcset="photo.webp">
  <img src="photo.jpg" alt="Product photograph">
</picture>

The browser picks the first format it supports. AVIF is the smallest (typically 50% smaller than JPEG), WebP is the middle ground, and JPEG is the universal fallback. This approach can shave hundreds of kilobytes off your page without any visual quality loss.

You can combine format selection with responsive sizing:

<picture>
  <source
    type="image/avif"
    srcset="
      photo-400.avif 400w,
      photo-800.avif 800w,
      photo-1200.avif 1200w
    "
    sizes="(max-width: 600px) 100vw, 800px"
  >
  <source
    type="image/webp"
    srcset="
      photo-400.webp 400w,
      photo-800.webp 800w,
      photo-1200.webp 1200w
    "
    sizes="(max-width: 600px) 100vw, 800px"
  >
  <img
    src="photo-800.jpg"
    srcset="
      photo-400.jpg 400w,
      photo-800.jpg 800w,
      photo-1200.jpg 1200w
    "
    sizes="(max-width: 600px) 100vw, 800px"
    alt="Product photograph"
  >
</picture>

Yeah, it's verbose. That's the trade-off. In practice, I generate this markup with build tools or templating logic. Nobody writes this by hand for every image.

Performance Tips

Always Set Width and Height

Add width and height attributes to your <img> tags. Browsers use these to calculate the aspect ratio and reserve space before the image loads, preventing layout shifts (CLS issues).

<img
  src="photo.jpg"
  width="800"
  height="600"
  alt="Sunset over mountains"
  style="width: 100%; height: auto;"
>

The CSS width: 100%; height: auto; makes it responsive while the HTML attributes give the browser the aspect ratio information. Best of both worlds.

Lazy Loading

<img
  src="photo.jpg"
  loading="lazy"
  alt="Below the fold content"
>

The loading="lazy" attribute defers loading until the image is near the viewport. Use it on every image except your above-the-fold hero. For the hero image, use loading="eager" (or just omit the attribute — eager is the default) and add fetchpriority="high":

<img
  src="hero.jpg"
  fetchpriority="high"
  alt="Hero banner"
>

Don't lazy-load your LCP image. The Largest Contentful Paint element should load as fast as possible. If your hero image has loading="lazy", you're actively hurting your performance score. I've seen this mistake on production sites from major brands.

The aspect-ratio CSS Property

Modern CSS gives you another tool for preventing layout shifts:

img {
  aspect-ratio: 16 / 9;
  width: 100%;
  height: auto;
  object-fit: cover;
}

This reserves the correct space regardless of whether the image has loaded. Combined with object-fit: cover, the image fills the space without distortion.

Generating Responsive Images

Nobody should be manually resizing images in Photoshop for responsive use. Use build tools:

# Using sharp-cli to generate multiple sizes
npx sharp-cli -i hero.jpg -o hero-400.jpg -- resize 400
npx sharp-cli -i hero.jpg -o hero-800.jpg -- resize 800
npx sharp-cli -i hero.jpg -o hero-1200.jpg -- resize 1200

# Convert to modern formats
npx sharp-cli -i hero.jpg -o hero.webp -- toFormat webp
npx sharp-cli -i hero.jpg -o hero.avif -- toFormat avif

If you're using a framework, there are even better options. Next.js has its built-in <Image> component. Astro has astro:assets. Vite has vite-imagetools. These generate responsive variants automatically during the build process.

Choosing Your Breakpoints

Don't just pick random image widths. Think about your layout breakpoints and the actual display sizes:

More sizes isn't always better. Each additional variant adds build time and storage. In my experience, 3-4 sizes per image covers the vast majority of scenarios without going overboard.

Convert Your Image Layouts to Images

Design responsive image grids in HTML and CSS, then export them as pixel-perfect images. Perfect for documentation and design specs.

Try HTML to PNG →

Responsive images aren't optional anymore — they're a performance requirement. Google measures Core Web Vitals, and oversized images directly impact your LCP and CLS scores. The syntax is admittedly clunky, but the payoff is real: faster loads, lower bandwidth costs, and better user experience on every device.

Start simple. Add srcset with 3-4 width variants to your most important images. Add loading="lazy" to everything below the fold. That alone will make a measurable difference. Then work your way up to <picture> for format selection and art direction as your pipeline matures.

Sachin Bhanushali
Written by

Sachin Bhanushali

Full-stack developer and creator of HTMLtoImages. Building free, privacy-first developer tools that run entirely in your browser.