10.5 Responsive Images: srcset Generation
Right, let’s talk about responsive images. You’ve been here before: you write a nice <img src="hero.jpg">, it looks perfect on your retina MacBook Pro, and then you get an angry email from someone on a spotty 3G connection whose phone just downloaded a 4MB file meant for a desktop. We’ve all been that villain. The goal is simple: serve the right image to the right device. The implementation, well, the W3C committee had a few cups of coffee and got thorough.
The modern solution is srcset and sizes. Don’t panic. It looks more intimidating than it is. At its core, srcset is just a polite way of giving the browser a menu of image options you’ve prepared. The browser, knowing its own viewport size, pixel density, and current network conditions better than you ever could, gets to choose the most appropriate one. It’s offloading that decision to the one entity actually in a position to make it. Smart.
The Two Flavors of srcset
This is the first point of confusion, so let’s clear it up immediately. srcset is used for two distinct, mutually exclusive use cases:
- Resolution Switching (Different image sizes for different screen densities): “Hey browser, here’s the same image at 1x, 2x, and 3x resolutions. Pick the one that suits your pixel density.” You use
xdescriptors for this. - Art Direction (Different crops or compositions for different viewports): “Hey browser, here’s a wide hero image for desktops, a square crop for tablets, and a tall vertical crop for mobile. Pick the one that suits your viewport width.” You use
wdescriptors and thesizesattribute for this.
Mixing these concepts in your head will lead to very broken, very sad images. Let’s break them down.
Resolution Switching with x-Descriptors
This is the simpler of the two. You’re serving the same composition of image, just at higher resolutions for fancy screens. The x describes the device’s pixel density (e.g., 1x for standard, 2x for Retina/Hi-DPI).
<img src="flower-1x.jpg"
srcset="flower-1x.jpg 1x,
flower-2x.jpg 2x,
flower-3x.jpg 3x"
alt="A vibrant, responsive flower">
Here’s the deal: the src is your mandatory, no-JavaScript, fall-back-for-incompetent-browsers default. Always include it. The srcset is a helpful hint. A browser that understands srcset will look at its own screen, see if it’s a 2x device, and likely choose flower-2x.jpg instead of the src. If it’s a 3x display, it’ll grab the even bigger one. This is pure performance/quality optimization.
Art Direction and Width (w) Descriptors
This is where the real power—and complexity—lies. You’re not just changing resolution; you’re changing the actual image. A wide landscape crop for desktop, a more square crop for tablet, and a close-up portrait crop for mobile. This is crucial for good art direction, not just performance.
For this, you use w descriptors which tell the browser the actual intrinsic width (in pixels) of each image source. This is non-negotiable. Lie to the browser here and the whole system falls apart.
But the browser needs a second piece of information: how big do you, the author, intend this image to be on the page? That’s what the sizes attribute is for. It’s a CSS-length describing the image’s layout width. The browser combines this knowledge with its own viewport width to pick an image.
<img src="hero-fallback.jpg"
srcset="hero-mobile.jpg 600w,
hero-tablet.jpg 900w,
hero-desktop.jpg 1200w"
sizes="(max-width: 768px) 100vw,
(min-width: 769px) 75vw,
1200px"
alt="A hero image that changes crop for art direction">
Let’s read that sizes attribute like a boss. It’s a comma-separated list of media queries and length values. The browser works down this list until it finds the first true media condition.
(max-width: 768px) 100vw- “If the viewport is 768px or less, I expect this image to be about 100% of the viewport width.”(min-width: 769px) 75vw- “Otherwise, if the viewport is 769px or more, I expect this image to be about 75% of the viewport width.”1200px- The default catch-all value. “If all else fails, or the browser doesn’t understand media queries, just assume it’ll be 1200px wide.”
The browser now does the math: “My viewport is 1000px wide. The sizes attribute tells me the image will be 75% of that, so 750px. Now, let’s look at my srcset options. I need an image that is at least 750px wide to look sharp. hero-tablet.jpg (900w) is the smallest one that’s bigger than 750px, so I’ll choose that to save bandwidth. Or, if I’m on a slow connection, I might even choose a smaller one and let it scale up. I’m the browser, I do what I want.” And you know what? It’s right.
The Sizes Attribute is Your Mental Model
The most common pitfall is whipping up a bunch of images, throwing them in a srcset with w descriptors, and then using a lazy sizes="100vw". You’re essentially telling the browser, “This image will always be the full width of the viewport,” which is almost never true outside of a full-bleed hero image. If your image is in a container with max-width: 1200px and margin: 0 auto, your sizes attribute should reflect that! Something like sizes="(min-width: 1200px) 1200px, 100vw" is far more accurate.
Generating This Stuff Without Losing Your Mind
Doing this by hand is for masochists and people writing tutorials. In the real world, you automate it. Most modern static site generators (Eleventy, Hugo) and front-end build processes (Webpack, Vite) have plugins that are godsends for this.
For example, using the eleventy-img plugin in Eleventy:
const Image = require("@11ty/eleventy-img");
module.exports = async function () {
let metadata = await Image("src/img/hero.jpg", {
widths: [300, 600, 900, 1200], // The "w" sizes to generate
formats: ["avif", "webp", "jpeg"], // Modern formats first!
outputDir: "_site/img/",
urlPath: "/img/",
});
// This returns the complete HTML string for you
return Image.generateHTML(metadata, {
alt: "My responsive hero image",
sizes: "(min-width: 1200px) 1200px, 100vw", // Your model
loading: "lazy",
decoding: "async",
});
};
This single script will generate 12 different images (4 sizes x 3 formats), optimize them, and output a perfect <picture> element with fallbacks. This is the way. The sizes attribute is still your job to get right—the plugin can’t know your layout—but it handles the brutal, repetitive part.
The bottom line: srcset and sizes are a conversation with the browser. You provide the options and your layout intentions, and it makes the best possible choice for the user. It’s a bit more work upfront, but it’s the difference between blasting users with a one-size-fits-all monstrosity and being a thoughtful, professional web developer. And frankly, your users deserve the latter.