
Responsive Images Done Right: srcset, sizes, and Art Direction
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:
- 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.
- 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%.
- 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:
- On screens up to 600px wide: the image takes up 100% of the viewport width
- On screens up to 1024px: it takes up 50% of the viewport width
- On everything else: it takes up 33% of the viewport width
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=crop
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=crop 400w,
https://api.getpronto.io/v1/file/hero.avif?w=800&h=800&fit=crop 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=crop 400w,
https://api.getpronto.io/v1/file/hero.webp?w=800&h=800&fit=crop 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:
- Small: 400px (phones)
- Medium: 800px (tablets, small laptops)
- Large: 1200px (desktops)
- Extra large: 1600px (large/retina desktops)
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
- Reduce Page Size by more than 50% with WebP and AVIF — pair responsive images with modern formats for maximum savings.
Ready to simplify your responsive image workflow? Try Get Pronto for free and start serving optimized images from a single upload.
