The bulletproof snippet
Copy this. It handles art direction, format fallback, and resolution switching in one block. The browser takes the first <source> whose media matches and whose type it supports, then picks the best width from that srcset.
<picture>
<!-- 1. ART DIRECTION: square crop on narrow screens -->
<source media="(max-width: 640px)"
type="image/avif"
srcset="hero-square-480.avif 480w,
hero-square-960.avif 960w"
sizes="100vw">
<source media="(max-width: 640px)"
type="image/webp"
srcset="hero-square-480.webp 480w,
hero-square-960.webp 960w"
sizes="100vw">
<source media="(max-width: 640px)"
srcset="hero-square-480.jpg 480w,
hero-square-960.jpg 960w"
sizes="100vw">
<!-- 2. WIDE CROP: best format the browser supports -->
<source type="image/avif"
srcset="hero-wide-640.avif 640w,
hero-wide-1280.avif 1280w"
sizes="(max-width: 900px) 100vw, 900px">
<source type="image/webp"
srcset="hero-wide-640.webp 640w,
hero-wide-1280.webp 1280w"
sizes="(max-width: 900px) 100vw, 900px">
<!-- 3. FALLBACK: plain <img> for ancient browsers -->
<img src="hero-wide-640.jpg"
srcset="hero-wide-640.jpg 640w,
hero-wide-1280.jpg 1280w"
sizes="(max-width: 900px) 100vw, 900px"
alt="Lighthouse on a coastal cliff at golden hour"
width="1280" height="549"
loading="lazy" decoding="async">
</picture>
<source> catches browsers that understand <picture> but neither AVIF nor WebP. The <img> catches browsers that predate <picture> entirely. Every path serves something.
Watch it work
The exact markup above, live. Resize your window across 640px and watch the crop change. The readout shows the file your browser actually chose.
—
Viewport: —
DPR: —
Crop: —
On a phone-width viewport you get the square crop; on desktop, the wide one. On a 2x display the browser reaches for the larger file in the winning srcset. Shrink this window and watch the chosen file change.
| File | Size | File | Size |
|---|---|---|---|
hero-wide-1280.jpg | 166 KB | hero-wide-1280.avif | 92 KB |
hero-wide-640.jpg | 48 KB | hero-wide-640.avif | 27 KB |
hero-square-960.jpg | 161 KB | hero-square-960.avif | 84 KB |
hero-square-480.jpg | 51 KB | hero-square-480.avif | 29 KB |
Same pixels, roughly 45% smaller. That is the entire format argument.
The three jobs
1. Art direction media
A wide landscape squeezed to phone width becomes a smear. media="(max-width: 640px)" swaps in a tighter square crop instead. Different image, same subject.
2. Format switching type
List AVIF first, WebP second, then a typeless JPEG source. The browser takes the first format it can decode. Modern browsers get half the bytes; nobody gets a broken image.
3. Resolution switching srcset + sizes
w descriptors say how wide each file is; sizes tells the browser how wide the slot is in your layout. It multiplies by DPR and picks. sizes must mirror your CSS or it downloads the wrong file.
Honest notes
- You do not always need
<picture>. If there is no art direction and you are fine serving one format family,<img srcset>alone does resolution switching with less markup. sizesis a promise about your layout. Lie about it and the browser fetches too much or too little. When in doubt, measure the rendered slot and write that down.- Keep
widthandheighton the<img>. They reserve space and prevent layout shift. Note the trade-off: art-directed crops change aspect ratio, so pair this with a CSSaspect-ratioper breakpoint if CLS matters to you. - The
<img>fallback is not optional. It is what ancient browsers render, it carries thealttext, and it is whereloadinganddecodinglive. - AVIF encodes slowly. Fine at build time, painful on the fly. If you generate variants per request, WebP is the pragmatic default and AVIF is the build-step upgrade.
media sources after the format sources. The browser takes the first matching source, so a media-less AVIF source placed first would win on mobile and serve the wide crop. Order is: art-directed groups first, formats best-first inside each group, <img> last.