Why responsive images?
A single fixed-size image is a compromise that satisfies no one. On a 4K desktop monitor, an 800-pixel-wide JPEG looks soft because the physical pixels are smaller than the CSS pixels it fills. On a mobile phone, that same 800-pixel file wastes hundreds of kilobytes of data — the device only needed a 400-pixel version.
The core idea is simple: prepare a handful of sizes of the same image, declare them in HTML, and let the browser pick the right one. The browser knows things you cannot know at authoring time — the current viewport width and the device's pixel ratio — so it can make a better decision than any server-side guess.
This tutorial covers the two resolution-switching mechanisms built into the
<img> element. Before you start, it helps to understand what a
CSS pixel actually is (see
03: Raster, vector & pixels for HiDPI
background) and why delivering the right file matters for loading performance (see
01: Why images matter for the golden rule on file size
and LCP).
Width descriptors and sizes
When an image is fluid — its rendered width changes with the viewport —
use width descriptors in srcset. A width descriptor is
just the image file's actual pixel width followed by a lowercase w, for
example 800w. This is the intrinsic pixel width of the source file on
disk, not a CSS value.
Alongside srcset, provide a sizes attribute that tells the
browser how wide the image will actually be rendered at each viewport breakpoint. The
browser cannot measure the rendered layout during its early prefetch pass — it reads
HTML before CSS is fully applied — so you have to declare this information yourself.
The browser's selection algorithm
The browser works through three steps to choose a file:
-
Evaluate
sizesagainst the current viewport to find the image's expected display width in CSS pixels (e.g., viewport is 480 px wide, sizes says(max-width: 600px) 100vw, so display width = 480 px). - Multiply by device pixel ratio (DPR). A Retina screen at 2× needs 960 physical pixels to fill 480 CSS pixels sharply.
-
Pick from
srcsetthe smallest file that is at least that many pixels wide. The browser may also factor in network conditions and previously cached versions.
Always include a plain src as a fallback. Browsers that predate
srcset are vanishingly rare, but src is required for
valid HTML and acts as the ultimate safety net.
Demo: fluid landscape with four sizes
Reading the sizes value: (max-width: 600px) 100vw means
"on screens up to 600 px wide, the image fills the full viewport width." The trailing
value 800px is the unconditional default — on wider screens the image is
capped at 800 CSS pixels. On a standard 1× display at 500 px wide the browser requests
the 400 w file; on a 2× Retina display at the same width it needs approximately 1000
physical pixels and jumps to the 1200 w file.
The width="800" height="533" attributes are not related to which file is
downloaded — they give the browser the image's aspect ratio so it can reserve the right
amount of vertical space in the layout before the file arrives, preventing Cumulative
Layout Shift (CLS).
Density descriptors: 1x, 2x, 3x
Some images are always rendered at a fixed CSS size: a navigation logo, an avatar placeholder, an icon. For these, width descriptors are unnecessary overhead — you already know the rendered width, so the only variable is the device's pixel ratio. Use density descriptors instead.
A density descriptor is the device pixel ratio followed by x: 1x
for standard screens, 2x for Retina/HiDPI screens (most modern phones and
many laptops), 3x for very high-density mobile screens. The browser picks
based solely on its current DPR — no sizes calculation needed.
Demo: logo at 1× and 2×
The image is always rendered at 200×80 CSS pixels. On a standard 1× display the
browser loads logo.png (200×80 actual pixels, a 1:1 match). On a 2×
Retina display it loads logo@2x.png (400×160 actual pixels, filling
the same CSS box with twice as many physical pixels, so the logo stays crisp
instead of appearing blurry).
The two mixing rules
There are exactly two rules you must follow when authoring srcset. Both
exist because the browser's selection algorithm changes fundamentally depending on
which descriptor type you use, and mixing them creates an ambiguity the spec
explicitly forbids.
Rule 1: never mix w and x in one srcset
Width descriptors (w) and density descriptors (x) use
different selection algorithms. Width descriptors require a sizes value
to derive the effective pixel requirement; density descriptors need only the DPR.
There is no way to reconcile the two calculations, so the HTML specification
disallows mixing them in a single srcset.
NOT ALLOWED
If you try this in a browser, the browser will either ignore all the invalid entries
or treat the descriptor as a parse error. The srcset attribute will
silently fall back to just the src URL. No error appears in the console —
it just quietly stops working. This is exactly the kind of bug that is hard to notice
but measurable in production analytics.
Rule 2: sizes is ignored when using density descriptors
The sizes attribute is only consulted during the width-descriptor algorithm.
If your srcset uses density descriptors, the browser ignores
sizes entirely and chooses based solely on DPR. Writing
sizes alongside density descriptors is not a parse error — the browser
just discards it — but it signals a misunderstanding of the mechanism and may confuse
future maintainers.
INEFFECTIVE
The sizes value here has no effect whatsoever. The browser will still
pick logo.png on 1× screens and logo@2x.png on 2× screens
regardless of what sizes says. The correct pattern for this image is
density descriptors without sizes, exactly as shown in the
demo above.
Quick reference
| Image type | Descriptor | Need sizes? |
What browser uses |
|---|---|---|---|
| Fluid (fills a % of viewport) | w |
Yes — required | Viewport width × DPR |
| Fixed CSS size (logo, icon) | x |
No — ignored | Device pixel ratio only |