Skip to content
Focal
Blog
  • html
  • responsive-images
  • performance

Responsive images with srcset and sizes, explained with worked examples

Learn how browsers use srcset width descriptors and the sizes attribute to pick an image file, with worked numbers for a hero banner and a product grid.

By Focal team 6 min read

A single src attribute sends the same file to a 390-pixel phone and a 2560-pixel monitor. The srcset and sizes attributes let the browser pick a file that matches the space the image fills and the screen’s pixel density. This post explains how the browser makes that choice, works through a hero banner and a product grid with real numbers, and lists the mistakes that make browsers download more pixels than they show.

How width descriptors work

A srcset with width descriptors lists the same image at several widths. Each entry is a URL, a space, and the file’s width in pixels followed by the letter w.

<img
  src="chair-800.jpg"
  srcset="chair-400.jpg 400w, chair-800.jpg 800w, chair-1600.jpg 1600w"
  sizes="(min-width: 800px) 50vw, 100vw"
  width="1600" height="1067"
  alt="Oak lounge chair">

MDN states that the w value must match the intrinsic width of the file. The browser does not download every candidate to check. It trusts the number you wrote.

The browser also needs the width of the slot the image will fill. An image near the top of the page can start loading before the browser has worked out the layout, so sizes gives it that width in advance. The width and height attributes do a separate job. They let the browser reserve space at the right aspect ratio before the file arrives.

With a candidate width and a slot width, the browser divides one by the other. The HTML standard calls the result the effective pixel density. A 1600w file in a 400-pixel slot has a density of 4x. The browser then chooses a candidate based on the screen’s pixel density, the zoom level and possibly network conditions. The standard leaves the final choice to the browser as “implementation-defined”, so treat the results below as the usual outcome, not a promise.

How the browser reads sizes

sizes is a comma-separated list. Every entry except the last has a media condition and a width. The browser uses the first entry whose condition is true and ignores everything after it. The last entry has no condition and acts as the default.

The width can be any CSS length, including px, vw and calc(). MDN notes that you cannot use a percentage. Media conditions test the viewport, not the image or its container.

If you leave sizes out and use w descriptors, the browser assumes 100vw, the full viewport width. The parsing algorithm in the HTML standard ends with the step “Return 100vw”. That default makes a missing sizes expensive for any image narrower than the viewport.

Worked example 1, a full-width hero

A hero banner spans the viewport at every breakpoint, so sizes is a single value.

<img
  src="hero-1280.jpg"
  srcset="hero-640.jpg 640w, hero-960.jpg 960w, hero-1280.jpg 1280w,
          hero-1920.jpg 1920w, hero-2560.jpg 2560w"
  sizes="100vw"
  width="2560" height="1280"
  alt="Living room with oak furniture">

Take a phone with a 390-pixel viewport and a device pixel ratio (DPR) of 3. The slot is 390 CSS pixels. The candidates work out to these densities:

  • 640w gives 640 ÷ 390 = 1.64x
  • 960w gives 960 ÷ 390 = 2.46x
  • 1280w gives 1280 ÷ 390 = 3.28x

The screen needs 3x, so the browser usually picks 1280w. MDN describes the usual rule. When no file matches exactly, the browser takes the first one bigger than the slot needs.

Now take a laptop with a 1440-pixel viewport at DPR 2. It needs 2880 device pixels. The largest candidate, 2560w, gives 1.78x, so the browser picks it. A 2880w file would match exactly. Whether it earns its extra bytes is a decision for your design team.

Worked example 2, a product grid

This grid has 16 pixels of padding on each side of the page and a 16-pixel gap between columns. It shows one column below 600 pixels, two columns from 600 pixels and four columns from 1024 pixels. The container stops growing at 1200 pixels.

The slot width at each breakpoint comes straight from that CSS:

  • Below 600px, one column fills the viewport minus the padding, so the slot is calc(100vw - 32px).
  • From 600px, two columns share the width minus padding and one gap, so the slot is calc(50vw - 24px).
  • From 1024px, four columns share the width minus padding and three gaps, so the slot is calc(25vw - 20px).
  • From 1200px, the container stops growing, so the slot is (1200 - 32 - 48) ÷ 4 = 280px.

Order the entries from widest to narrowest, because the first match wins.

<img
  src="chair-640.jpg"
  srcset="chair-320.jpg 320w, chair-480.jpg 480w, chair-640.jpg 640w,
          chair-960.jpg 960w, chair-1280.jpg 1280w"
  sizes="(min-width: 1200px) 280px,
         (min-width: 1024px) calc(25vw - 20px),
         (min-width: 600px) calc(50vw - 24px),
         calc(100vw - 32px)"
  width="1280" height="1280"
  loading="lazy"
  alt="Oak lounge chair">

Here is what four example screens request:

Screen Viewport DPR Slot Pixels needed Likely pick
Phone 390px 3 358px 1074 1280w
Tablet 820px 2 386px 772 960w
Laptop 1440px 2 280px 560 640w
Desktop monitor 1920px 1 280px 280 320w

Remove the sizes attribute and the laptop row changes. The browser assumes a 1440-pixel slot, needs 2880 pixels, and downloads the largest candidate, 1280w. That file has four times the pixels of the 640w file it needed.

Mistakes that cost bytes

Watch for these five mistakes:

  • A missing sizes attribute makes every thumbnail behave like a full-width image, as the laptop row above shows.
  • A w value that does not match the file breaks the density calculation. A 1200-pixel file labelled 800w looks less sharp to the browser than it is, so the browser can pass it over for a bigger file.
  • Mixing w and x descriptors in one srcset is invalid. MDN says so directly.
  • Testing by dragging the window narrower can mislead you. web.dev notes that most browsers will not request a smaller candidate when a larger one is already in the cache. Test with the cache disabled.
  • Using sizes to show a different crop on phones is the wrong tool. A different crop is art direction, and MDN recommends the <picture> element with media attributes for that.

sizes=“auto” for lazy-loaded images

The auto keyword tells the browser to use the width the layout gives the image. MDN states that it is valid only with loading="lazy", because a lazy image loads after layout information exists. MDN’s compatibility data lists support in Chrome and Edge from version 126 and in Firefox from version 150. Safari does not support it as of September 2026.

The HTML standard allows a fallback list after auto. Browsers without support skip the keyword and read the rest.

sizes="auto, (min-width: 1200px) 280px, (min-width: 600px) calc(50vw - 24px), calc(100vw - 32px)"

Keep explicit sizes values on images near the top of the page. Those images should not be lazy-loaded, so auto does not apply to them.

x descriptors for fixed-width images

A logo that is always 200 CSS pixels wide does not need sizes. List density descriptors instead, and the browser matches them to the screen.

<img src="logo-200.png" srcset="logo-200.png 1x, logo-400.png 2x" alt="Store logo">

Producing the files

Every width in srcset has to exist as a file. A build step can write them to disk, or an image service can create them from the URL. Focal takes the second approach. The size is part of the URL path, so 800x0 asks for an image 800 pixels wide with the height following the source’s aspect ratio. Focal transforms each candidate once and then serves it from the edge cache. The Focal docs cover the full URL format.

<img
  src="https://img.focaltool.com/your-key/unsafe/640x0/products/chair.jpg"
  srcset="https://img.focaltool.com/your-key/unsafe/320x0/products/chair.jpg 320w,
          https://img.focaltool.com/your-key/unsafe/640x0/products/chair.jpg 640w,
          https://img.focaltool.com/your-key/unsafe/1280x0/products/chair.jpg 1280w"
  sizes="(min-width: 1200px) 280px,
         (min-width: 1024px) calc(25vw - 20px),
         (min-width: 600px) calc(50vw - 24px),
         calc(100vw - 32px)"
  width="1280" height="1280"
  alt="Oak lounge chair">

To check your own grid, write the sizes value from your CSS breakpoints first. Then emulate a 390-pixel phone at DPR 3 and a 1440-pixel laptop at DPR 2 in DevTools with the cache disabled. The Network panel should show the 1280w file on the phone and the 640w file on the laptop.

Sources

More from the blog

All posts