Responsive Images in Depth

A 2,000-pixel photo suits a desktop hero but wastes mobile data on a phone showing it 375 pixels wide. Resolution switching serves the same picture at different pixel sizes and lets the browser pick (srcset, sizes); art direction serves different crops for different layouts, chosen by you (<picture>, media).

A fixed-size image such as a logo needs only density descriptors: srcset="logo-200.png 1x, logo-400.png 2x". A fluid image uses width descriptors (each file's real pixel width) plus sizes, the width of the image slot, because the browser picks a file while parsing, before CSS layout exists. sizes lists media conditions with lengths (px, em, vw, calc(), but not percentages); the first match wins and the default is 100vw.

How a browser chooses a file from srcset and sizes (1280 px viewport, 2x screen)
How a browser chooses a file from srcset and sizes (1280 px viewport, 2x screen)

The same markup on other devices shows why the phone, at full width on a 3x screen, deserves a 1200w candidate.

srcset="...320w, ...480w, ...800w" sizes="(width <= 600px) 100vw, 280px"
Device Viewport, DPR Slot Pixels needed File chosen
Phone 375 px, 3x 100vw = 375 px 1,125 800w (largest)
Laptop 1,280 px, 1x 280 px 280 320w
Desktop 1,440 px, 1.5x 280 px 420 480w

The demo prints currentSrc, the URL actually chosen, under each image (the renderer uses a 2x screen).

Resolution switching and art direction, reporting currentSrcHTML
<!doctype html>
<html lang="en">
  <meta charset="utf-8">
  <base href="https://cdn.jsdelivr.net/gh/mdn/learning-area@main/html/multimedia-and-embedding/">
  <style>
    body { font: 13px system-ui, sans-serif; margin: 10px; display: flex; gap: 12px;
           align-items: start; }
    figure { margin: 0; } img { display: block; height: auto; }
    figcaption { font: 12px monospace; }
  </style>
  <figure>
    <img srcset="responsive-images/elva-fairy-320w.jpg 320w,
                 responsive-images/elva-fairy-480w.jpg 480w,
                 responsive-images/elva-fairy-800w.jpg 800w"
         sizes="(width <= 600px) 100vw, 160px" src="responsive-images/elva-fairy-480w.jpg"
         width="480" height="320" style="width: 160px" alt="Girl in a fairy costume">
    <figcaption></figcaption>
  </figure>
  <figure>
    <picture>
      <source media="(width <= 500px)" srcset="responsive-images/elva-480w-close-portrait.jpg"
              width="480" height="552">
      <img src="responsive-images/elva-800w.jpg" width="800" height="464" style="width: 200px"
           alt="Elva and her father">
    </picture>
    <figcaption></figcaption>
  </figure>
  <script>
    addEventListener('load', () => {
      for (const img of document.images) {
        img.closest('figure').querySelector('figcaption').textContent =
          `DPR ${devicePixelRatio}: ${img.currentSrc.split('/').pop()}`;
      }
    });
  </script>
Browser output of Listing 2.23
Browser output of 23

The 160 px slot needs 320 device pixels and gets the 320w file; change both to 280 px and it switches to 800w, as in the diagram. The second figure is art direction: at 500 px or narrower the matching <source> supplies a portrait close-up, and at 720 px the <img> falls back to the wide shot. Only the <img> renders, so alt, loading, styles and listeners belong on it; width and height on each <source> reserve that crop's aspect ratio.

For lazy images, sizes="auto" uses the real rendered width, since they load after layout. It works in Chrome 126 1 + and Firefox 150 555 +; other browsers skip auto and use the fallback lengths after it: sizes="auto, 280px".

Pitfalls: a wrong sizes silently wastes bytes, so compare img.currentSrc with the rendered width; never mix w and x descriptors in one srcset; and preload a responsive LCP image with imagesrcset and imagesizes on <link rel="preload"> (Performance Hints in Markup).