Responsive images come down to two attributes on the <img> tag. srcset lists the files you have, and sizes describes how wide the image will appear on the page. From those two, the browser picks the smallest file that will still look sharp on the visitor's screen.
Once you see how the browser uses them, they're simple. Most problems come from a sizes value that doesn't match the layout.
The short version
<img
src="photo-768.jpg"
srcset="photo-480.jpg 480w, photo-768.jpg 768w, photo-1200.jpg 1200w, photo-1600.jpg 1600w"
sizes="(max-width: 768px) 100vw, 768px"
width="1600" height="1067"
alt="A bowl of ramen on a wooden table">
srcsetlists each file with its real width in pixels, written with aw.photo-1200.jpgis 1200 pixels wide, so it's1200w.sizessays how wide the image will display: here, the full width of the screen on screens up to 768 px wide, and 768 px everywhere else.srcis the fallback for very old browsers.widthandheightgive the image's proportions, so the browser can reserve space before it loads and the page doesn't jump.
How the browser picks a file
The browser goes through three steps:
- It reads
sizesleft to right and uses the first condition that matches the screen. That gives the display width in CSS pixels. - It multiplies that width by the screen's pixel density. Most phones are 2x or 3x, many laptops are 2x, and older monitors are 1x.
- It picks a file from
srcsetthat's at least that many pixels wide.
Run the example above through those steps:
- On a phone 390 px wide at 3x,
100vwis 390 px. Times 3 is 1170, so the browser takesphoto-1200.jpg. - On a 2x laptop showing the image at 768 px, 768 times 2 is 1536. It takes
photo-1600.jpg. - On a 1x desktop monitor, 768 times 1 is 768. It takes
photo-768.jpg.
The browser has some freedom in that last step. It may use a bigger file that's already in its cache, or pick a smaller one when the visitor has data saving turned on.
Why sizes exists
It seems odd that you have to tell the browser how wide the image will be. The reason is timing. Browsers start downloading images as soon as they see the HTML, before the CSS has loaded and long before the page is laid out. At that point the browser has no idea how wide your column is. sizes tells it ahead of time.
Leave sizes out and the browser assumes the image fills the whole screen. On a wide monitor with a narrow content column, that means downloading a much bigger file than it needs.
Writing a good sizes value
A sizes value is a comma-separated list of conditions, each followed by a width, ending with one width that has no condition. The first match wins, so order matters.
You can use vw (a percentage of the screen width), px and calc(). You can't use percentages, because those depend on a layout the browser doesn't have yet.
A few common layouts:
<!-- Content column, never wider than 768 px -->
sizes="(max-width: 768px) 100vw, 768px"
<!-- Full-width hero -->
sizes="100vw"
<!-- Card grid: 1 across on phones, 2 on tablets, 3 on desktops, 1280 px max -->
sizes="(min-width: 1280px) 400px, (min-width: 1024px) 31vw, (min-width: 640px) 48vw, 100vw"
It doesn't need to be exact. Within 10 or 20 percent is fine, since the browser rounds up to the next file anyway. What matters is not being wildly off, like saying 100vw for an image that displays at a third of the screen.
How many widths to make
Four to six widths cover most layouts. Start with the smallest size the image appears at, end at about twice the largest size it displays at, and space the rest out evenly.
For a 768 px content column, 480, 768, 1200 and 1600 work well. 1600 covers the column on a 2x laptop, and 1200 covers phones at 3x. Going much past twice the display width adds file size you won't see.
Fixed-size images: use x descriptors
For images that are always the same size, like a logo or an avatar, you don't need sizes at all. Use x descriptors for pixel density instead:
<img src="logo.png" srcset="logo.png 1x, logo@2x.png 2x" width="160" height="40" alt="FileTaco">
The browser takes logo@2x.png on 2x screens and logo.png everywhere else. Don't mix w and x descriptors in the same srcset.
srcset inside a picture element
To offer AVIF and WebP with a JPEG fallback, wrap the image in <picture> and give each format its own <source>:
<picture>
<source type="image/avif" srcset="photo-480.avif 480w, photo-768.avif 768w, photo-1200.avif 1200w" sizes="(max-width: 768px) 100vw, 768px">
<source type="image/webp" srcset="photo-480.webp 480w, photo-768.webp 768w, photo-1200.webp 1200w" sizes="(max-width: 768px) 100vw, 768px">
<img src="photo-768.jpg" srcset="photo-480.jpg 480w, photo-768.jpg 768w, photo-1200.jpg 1200w" sizes="(max-width: 768px) 100vw, 768px" width="1200" height="800" alt="A bowl of ramen on a wooden table">
</picture>
Every <source> needs its own sizes. You still need the <img>: it's the element that actually displays, and it carries the alt text and dimensions.
<picture> can also handle art direction, with a media attribute on each <source> to show a tighter crop on phones, for example. For the same picture at different sizes, srcset and sizes are all you need.
sizes="auto" for lazy-loaded images
Images with loading="lazy" are a special case. The browser waits until they're about to scroll into view, and by then the page is laid out, so it can measure the real width itself. Adding auto lets it:
<img loading="lazy" sizes="auto, (max-width: 768px) 100vw, 768px" srcset="..." src="..." alt="...">
Chromium browsers and Firefox support it. Browsers that don't just ignore auto and use the rest of the list, which is why the fallback after it matters. auto only works together with loading="lazy", and WordPress 6.7 and later add it to lazy-loaded images automatically.
Common mistakes
- w descriptors with no sizes. The browser assumes
100vwand downloads oversized files on wide screens. - A sizes value that doesn't match the layout. Often copied from another site, or left at a theme default after the layout changed.
- Lazy-loading the main image. The big image at the top of the page should load immediately. Remove
loading="lazy"from it and addfetchpriority="high". - Missing width and height. Without them the page jumps as images load, which PageSpeed Insights reports under Layout shift culprits.
- Listing the wrong widths. The number before
whas to be the file's real width. If it's wrong, the browser is choosing based on bad information.
Check which file the browser chose
In Chrome, right-click the image, choose Inspect, then open the Console and type $0.currentSrc. That's the file the browser actually loaded. To try other screens, turn on device emulation with the phone and tablet icon in DevTools and reload.
The cache can fool you here: once the browser has a large file, it keeps using it. Check Disable cache in the Network tab, or test in a private window.
To make the files and the markup in one step, try the responsive image converter. Pick your widths and a layout preset for sizes, and it creates WebP, AVIF and JPEG versions and writes the <picture> code. If a PageSpeed Insights warning brought you here, How to fix "Improve image delivery" covers the rest.