HTML media · 2 / 7
lesson 2

Responsive images: srcset and sizes

How the browser picks the right image file for the right device — and why sizes is the puzzle most people get wrong.

~ 17 min read·lesson 2 of 7
0 / 7

You ship a 2000px-wide hero image because some users have retina screens. A user on a 4G phone with a 360px-wide screen downloads all 2 MB of it. They wait. You waste their data. They bounce.

That problem is what srcset and sizes solve. They let you offer the browser several versions of the same image at different sizes, and the browser picks the best one for the user's screen and connection.

This is one of the highest-leverage performance changes you can make to a site — and one of the most consistently misunderstood attributes in HTML. Let's slow down and unpack it.

The problem srcset solves

Take a hero image. Different users see it at very different sizes:

  • A laptop user with a 1440px wide screen sees the image at maybe 1200px.
  • An iPhone user sees it at 390px.
  • A retina iPhone user also sees it at 390px CSS pixels but with 2x device pixels — so a 780px image gives sharp results.

If you ship a single 2000px file, the iPhone user downloads 5x more data than they will ever use. If you ship a single 800px file, the laptop user gets a blurry hero.

srcset lets you list several files; sizes tells the browser how big the image will display. The browser does the math and picks the smallest file that still looks sharp.

The x descriptor — pixel density

The simplest form of srcset uses the x descriptor. You provide a 1x, 2x, and maybe 3x version of the same image at the same display size.

x-descriptors.html
<img
src="logo.png"
srcset="logo.png 1x, logo@2x.png 2x, logo@3x.png 3x"
alt="Daily Bread Bakery">

Reading the srcset: logo.png is for 1x screens, logo@2x.png is for 2x screens, logo@3x.png is for 3x screens.

Use x descriptors when the image displays at a fixed size — a logo, an icon, an avatar. The display size doesn't change, only the device pixel density.

The browser picks based on the device's devicePixelRatio. On a regular monitor, it picks logo.png. On a retina laptop (2x), it picks logo@2x.png. On an iPhone Pro (3x), it picks logo@3x.png.

src is the fallback for browsers that don't understand srcset (vanishingly few in 2026, but cheap insurance).

The w descriptor and sizes

For images that resize across breakpoints — heroes, article images, full-bleed photos — the x descriptor is not enough. You don't know in advance how many pixels the image will occupy. You need the w descriptor.

The w descriptor tells the browser the intrinsic width of each candidate file. The sizes attribute tells it how big the image will display.

w-and-sizes.html
<img
src="loaf-800.jpg"
srcset="loaf-400.jpg 400w,
        loaf-800.jpg 800w,
        loaf-1600.jpg 1600w,
        loaf-3200.jpg 3200w"
sizes="(min-width: 64em) 800px, 100vw"
alt="A round sourdough loaf"
width="800" height="600">

Reading top to bottom: there are four versions of the loaf at different intrinsic widths (400, 800, 1600, 3200 pixels). The sizes attribute says "if the viewport is at least 64em wide, the image displays at 800 CSS pixels; otherwise it fills the viewport (100vw)".

The browser does this math:

  1. Look at the viewport width. Suppose the user is on a 360px-wide phone.
  2. Match against sizes. The viewport is less than 64em (about 1024px), so the size is 100vw = 360 CSS pixels.
  3. Multiply by device pixel ratio. The phone is 2x, so 360 × 2 = 720 actual device pixels.
  4. Pick the smallest file in srcset that is at least 720w. That's loaf-800.jpg.

The phone user gets the 800w file. A retina laptop displaying the image at 800 CSS pixels gets the 1600w file. The 3200w file is for very high-density 4K monitors and zoomed-in displays.

A useful picture: srcset is the menu of available sizes; sizes is the rule for predicting which size the user will eat.

viewport e.g. 360pxsizes → 100vw = 360×2 dpr need 720srcset candidates400w800w ✓1600w3200w
srcset offers four sizes; sizes tells the browser the image's display width; the browser picks the smallest file that won't look blurry.
check your understanding
You write srcset="img-800.jpg 800w, img-1600.jpg 1600w" with no sizes attribute. What does the browser assume?

Writing sizes the right way

sizes is a list of media-query-and-length pairs. The browser walks the list and picks the first one whose media query matches.

sizes-syntax.html
sizes="
(min-width: 1280px) 800px,
(min-width: 768px)  50vw,
100vw
"

Reading: on viewports 1280px+, the image displays at 800 CSS pixels. On viewports 768px+, the image is 50% of the viewport. Below 768px, the image is 100% of the viewport.

The last entry in sizes has no media query — it is the catch-all default.

The lengths can be CSS units (px, em, rem) or viewport units (vw). Calc expressions are allowed: calc(100vw - 4rem). What is not allowed: percentages of the parent. The browser does not know the parent's size when it makes the decision; everything must resolve relative to the viewport.

A common pattern: an article image that takes the article column. The article's max-width might be 64ch in CSS, but sizes cannot say "64ch" because that is parent-relative. Instead, sizes approximates the eventual display width as a fraction of the viewport at each breakpoint.

article-image.html
<img
src="loaf-800.jpg"
srcset="loaf-400.jpg 400w, loaf-800.jpg 800w, loaf-1600.jpg 1600w"
sizes="(min-width: 64em) 720px, 100vw"
alt="..."
width="800" height="600">

The article column is roughly 720px on desktop (CSS max-width: 45rem + padding); below the breakpoint, the image is full-width.

Tip

Don't sweat sizes precision down to the pixel. The browser only uses it to pick from your srcset candidates. As long as sizes is in the right ballpark for each breakpoint, the browser picks well.

Two common mistakes

Mistake 1: mixing x and w descriptors. They cannot coexist in one srcset. Pick one. x for fixed-size images; w for resizing images.

broken.html
<!-- broken: mixing x and w -->
<img srcset="logo.png 1x, logo-large.png 800w" src="logo.png" alt="...">

The browser ignores everything after the first descriptor type; the result is unpredictable.

Mistake 2: forgetting sizes with w descriptors. Without sizes, the browser assumes the image fills the viewport (100vw), which often picks a file that is way too large. The mismatch wastes bandwidth.

broken-sizes.html
<!-- broken: w descriptors but no sizes — browser assumes 100vw -->
<img
src="thumb-200.jpg"
srcset="thumb-200.jpg 200w, thumb-400.jpg 400w"
alt="...">

If the thumbnail actually displays at 200px, you want sizes="200px". Without it, the browser thinks the thumbnail occupies the full viewport and downloads the larger file every time.

check your understanding
An article-inline image displays at 720px on desktop and full-viewport on mobile. The breakpoint is 1024px. Which sizes is closest to right?
check your understanding
You write srcset="logo.png 1x, logo@2x.png 2x" for a logo that always displays at 200px. The user is on a 2x retina screen. Which file is downloaded?
check your understanding
You ship the same image for a 1600px-wide hero on desktop and a 360px-wide thumbnail in a list. You're told to use one <img> with srcset. The right approach is:
← prevnext lesson →
KeepLearningcertificate
for completing
HTML media
0 of 7 read