Responsive Images Done Right: srcset, sizes, and Art Direction

Responsive Images Done Right: srcset, sizes, and Art Direction

By Get Pronto

Here's a scenario that plays out on millions of websites every day: someone pulls out their phone, visits your site, and their browser dutifully downloads a 2400px wide hero image—then squishes it down to fit a 375px screen. They just burned through mobile data for pixels they'll never see.

The frustrating part? Browsers have had the tools to avoid this for years. The srcset and sizes attributes, along with the <picture> element, give you fine-grained control over which image gets served to which device. But the syntax is genuinely confusing the first time you encounter it, and most tutorials either oversimplify things or drown you in edge cases.

This guide is the middle ground. We'll cover what actually matters, skip the stuff that doesn't, and by the end you'll be able to write responsive image markup that works properly across devices.

Why this matters more than you think

A quick reality check on where your bandwidth is going. Images still make up roughly 75% of the average page's total weight. On a typical product page with 8-10 images, you might be looking at 3-4MB of image data on desktop. If you're serving those same files to mobile users, you're essentially tripling or quadrupling the load time they actually need.

This hits you in three places:

  1. Core Web Vitals: Images are responsible for about 42% of LCP (Largest Contentful Paint) measurements. Serving oversized images to mobile devices is one of the fastest ways to tank your LCP score.
  2. Bounce rates on mobile: Mobile users are significantly less patient than desktop users. Every extra second of load time increases bounce probability by roughly 32%.
  3. Bandwidth costs: If you're paying per GB of transfer (and you probably are), serving 2400px images to phones is literally throwing money away.

The good news is that responsive images aren't that complicated once you understand the mental model behind them.

The img tag's secret weapon: srcset

The basic <img> tag has a single src attribute—one URL, one image, every device gets the same thing. The srcset attribute changes that by giving the browser a list of the same image at different widths.

<img
  src="photo.jpg"
  srcset="photo-400.jpg 400w,
          photo-800.jpg 800w,
          photo-1200.jpg 1200w,
          photo-1600.jpg 1600w"
  sizes="100vw"
  alt="A product photograph">

The w descriptor after each URL tells the browser "this file is X pixels wide." That's it. You're not telling the browser which one to use—you're giving it a menu and letting it choose based on the device's screen size and pixel density.

This is an important distinction. The browser makes the final call. A phone with a 2x Retina display and a 375px wide viewport might pick the 800w version (375 × 2 = 750, so the 800w is the closest fit). A standard 1080px laptop screen might grab the 1200w version.

The src attribute acts as a fallback for browsers that don't support srcset, which at this point is basically just IE11. But it's still good practice to include it.

The sizes attribute: telling the browser how big the image will be

Here's where people get tripped up. The srcset tells the browser what's available, but the browser also needs to know how much space the image will actually occupy in the layout. That's what sizes does.

Why can't the browser just figure this out from CSS? Because it starts downloading images before the CSS is fully parsed. It's a performance optimization built into every major browser—the preload scanner kicks in early and starts fetching images before layout is calculated. So sizes is your way of giving the browser a hint about the layout before it has that information.

<img
  src="photo.jpg"
  srcset="photo-400.jpg 400w,
          photo-800.jpg 800w,
          photo-1200.jpg 1200w"
  sizes="(max-width: 600px) 100vw,
         (max-width: 1024px) 50vw,
         33vw"
  alt="A product photograph">

Reading this sizes attribute from top to bottom:

The browser combines this with the viewport size and pixel density to pick the best candidate from srcset. On a 375px phone at 2x, with sizes="100vw", it knows it needs an image that's roughly 750px wide—so it grabs the 800w version.

Common sizes values for typical layouts

You don't need to overthink this. Here are the patterns that cover 90% of real-world use cases:

Layout sizes value
Full-width hero 100vw
Two-column grid (max-width: 768px) 100vw, 50vw
Three-column grid (max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw
Sidebar + content (max-width: 768px) 100vw, 66vw
Fixed-width thumbnail 200px

A quick note: sizes doesn't need to be pixel-perfect. The browser uses it as a rough guide. Being off by 50px isn't going to cause problems—being off by 500px will.

Art direction with <picture>

srcset and sizes solve the resolution switching problem—same image, different sizes. But sometimes you need a fundamentally different image depending on the screen size. That's art direction.

Think about a wide panoramic banner on desktop. On mobile, that panoramic shot becomes a tiny, unreadable strip. What you actually want is a tighter crop that focuses on the subject. The <picture> element handles this:

<picture>
  <source
    media="(max-width: 600px)"
    srcset="hero-mobile.jpg 600w, hero-mobile-2x.jpg 1200w"
    sizes="100vw">
  <source
    media="(max-width: 1024px)"
    srcset="hero-tablet.jpg 1024w, hero-tablet-2x.jpg 2048w"
    sizes="100vw">
  <img
    src="hero-desktop.jpg"
    srcset="hero-desktop.jpg 1600w, hero-desktop-2x.jpg 3200w"
    sizes="100vw"
    alt="Product hero shot">
</picture>

With <picture>, the browser evaluates <source> elements top to bottom and uses the first one where the media query matches. Unlike srcset where the browser has discretion, <picture> with media is a hard rule—if the media query matches, that source wins.

This is also where you handle format fallbacks:

<picture>
  <source type="image/avif" srcset="photo.avif">
  <source type="image/webp" srcset="photo.webp">
  <img src="photo.jpg" alt="Fallback for older browsers">
</picture>

The browser picks the first format it supports. AVIF for modern Chrome/Firefox/Safari, WebP as a middle ground, and JPEG as the universal fallback.

Combining everything: format + resolution + art direction

In practice, you often want all three: different crops for different screen sizes, multiple resolutions for each crop, and modern formats served where supported. The markup gets verbose, but it's not complicated—just layered:

<picture>
  <!-- Mobile crop, AVIF -->
  <source
    media="(max-width: 600px)"
    type="image/avif"
    srcset="hero-mobile.avif?w=400 400w,
            hero-mobile.avif?w=800 800w"
    sizes="100vw">

  <!-- Mobile crop, WebP fallback -->
  <source
    media="(max-width: 600px)"
    type="image/webp"
    srcset="hero-mobile.webp?w=400 400w,
            hero-mobile.webp?w=800 800w"
    sizes="100vw">

  <!-- Desktop, AVIF -->
  <source
    type="image/avif"
    srcset="hero-desktop.avif?w=800 800w,
            hero-desktop.avif?w=1200 1200w,
            hero-desktop.avif?w=1600 1600w"
    sizes="100vw">

  <!-- Desktop, WebP fallback -->
  <source
    type="image/webp"
    srcset="hero-desktop.webp?w=800 800w,
            hero-desktop.webp?w=1200 1200w,
            hero-desktop.webp?w=1600 1600w"
    sizes="100vw">

  <!-- Ultimate fallback -->
  <img
    src="hero-desktop.jpg?w=1200"
    alt="Hero image"
    loading="eager"
    width="1600"
    height="900">
</picture>

Yeah, that's a lot of markup for one image. This is exactly why most teams reach for a tool that generates responsive URLs automatically instead of maintaining this by hand.

Simplifying all of this with Get Pronto

Manually creating 4-6 versions of every image, converting each to multiple formats, and writing out the full <picture> markup is... not fun. It's the kind of work that's easy to get wrong and tedious to get right.

With Get Pronto, you upload your original high-resolution image once, and then generate any size or format on the fly through URL parameters:

Original:   https://api.getpronto.io/your-account/images/hero.jpg
800px wide: https://api.getpronto.io/your-account/images/hero.jpg?w=800
As WebP:    https://api.getpronto.io/your-account/images/hero.webp?w=800
As AVIF:    https://api.getpronto.io/your-account/images/hero.avif?w=800
Cropped:    https://api.getpronto.io/your-account/images/hero.jpg?w=600&h=600&fit=cover

No pre-generating variants. No build scripts. No managing dozens of files per image. The first request triggers the transformation, Get Pronto caches the result on a global CDN, and every subsequent request is served from the edge.

Here's what that verbose <picture> block looks like when you let Get Pronto handle the heavy lifting:

<picture>
  <source
    media="(max-width: 600px)"
    type="image/avif"
    srcset="https://api.getpronto.io/v1/file/hero.avif?w=400&h=400&fit=cover 400w,
            https://api.getpronto.io/v1/file/hero.avif?w=800&h=800&fit=cover 800w"
    sizes="100vw">
  <source
    media="(max-width: 600px)"
    type="image/webp"
    srcset="https://api.getpronto.io/v1/file/hero.webp?w=400&h=400&fit=cover 400w,
            https://api.getpronto.io/v1/file/hero.webp?w=800&h=800&fit=cover 800w"
    sizes="100vw">
  <source
    type="image/avif"
    srcset="https://api.getpronto.io/v1/file/hero.avif?w=800 800w,
            https://api.getpronto.io/v1/file/hero.avif?w=1200 1200w,
            https://api.getpronto.io/v1/file/hero.avif?w=1600 1600w"
    sizes="100vw">
  <source
    type="image/webp"
    srcset="https://api.getpronto.io/v1/file/hero.webp?w=800 800w,
            https://api.getpronto.io/v1/file/hero.webp?w=1200 1200w,
            https://api.getpronto.io/v1/file/hero.webp?w=1600 1600w"
    sizes="100vw">
  <img
    src="https://api.getpronto.io/v1/file/hero.jpg?w=1200"
    alt="Hero image"
    loading="eager"
    width="1600"
    height="900">
</picture>

Same result, but you only uploaded one file. Get Pronto generated every size and format variant on demand, cached them globally, and serves them from the nearest edge location. You just write the markup.

A few things people get wrong

After seeing a lot of responsive image implementations in the wild, here are the mistakes that come up over and over:

Forgetting width and height on the <img> tag

Modern browsers use the width and height attributes to calculate the aspect ratio before the image loads. This prevents layout shift (CLS). Always include them, even if you're using CSS to control the actual display size.

<!-- Do this -->
<img src="photo.jpg" width="1600" height="900" alt="...">

<!-- Not this -->
<img src="photo.jpg" alt="...">

Using srcset without sizes

If you use width descriptors (w) in srcset but don't include a sizes attribute, the browser assumes sizes="100vw"—meaning it thinks the image is full-width. For a small thumbnail in a sidebar, that means the browser downloads a much larger image than necessary.

Going overboard with breakpoints

You don't need an image for every 100px increment. The browser interpolates between your options, and the difference between serving a 780px image vs an 800px image is negligible. Three to five sizes per image is plenty for most cases:

Using loading="lazy" on above-the-fold images

Lazy loading is great for images below the fold—it defers loading until the user scrolls near them. But applying it to your hero image or anything visible on initial page load actually hurts performance by delaying the LCP element. Use loading="eager" (or just omit the attribute) for above-the-fold images.

Quick reference cheat sheet

Here's a decision tree for choosing the right approach:

Same image, just different sizes? → Use srcset + sizes on <img>

Different crops or compositions per screen size? → Use <picture> with media queries

Serving modern formats with fallbacks? → Use <picture> with type attributes

All of the above? → Combine <picture> sources with srcset and sizes on each

Don't want to manage multiple image files? → Use a service like Get Pronto that generates variants from a single upload via URL parameters

Start serving the right pixels

Responsive images aren't bleeding-edge technology—they've been well-supported across browsers for years now. But they're still underused, mostly because the syntax is unintuitive and the manual workflow of generating variants is tedious.

The fix is straightforward: use srcset and sizes for resolution switching, <picture> for art direction and format fallbacks, and a service like Get Pronto to avoid maintaining dozens of image files per asset.

Your mobile users (and your bandwidth bill) will thank you.

Related reading

Ready to simplify your responsive image workflow? Try Get Pronto for free and start serving optimized images from a single upload.

Tags

OptimizationImagesGuides