← dominic.balass.one

The <picture> element, done right

One image, three jobs: serve a different crop on small screens, pick the smallest modern format the browser understands, and never download more pixels than the layout needs. Below is the snippet I actually reuse, then a live demo proving which file your browser picks.

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-bulletproof.html
<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>
Why this order matters. Media-constrained sources go first, most capable format first within each group. The typeless JPEG <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.

Lighthouse on a coastal cliff at golden hour
Chosen file: — Viewport: — DPR: — Crop: —
AVIF: … WebP: …

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.

FileSizeFileSize
hero-wide-1280.jpg166 KBhero-wide-1280.avif92 KB
hero-wide-640.jpg48 KBhero-wide-640.avif27 KB
hero-square-960.jpg161 KBhero-square-960.avif84 KB
hero-square-480.jpg51 KBhero-square-480.avif29 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

Common mistake: putting the 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.