Web Platform · HTML and Semantics
How srcset and sizes Select an Image Resource
Width and density descriptors describe the candidates, the sizes list converts layout into a pixel width, and the choice among candidates belongs partly to the browser.
An img element with srcset="small.jpg 400w, large.jpg 1200w" and sizes="(min-width: 60em) 30em, 100vw" carries two independent pieces of information: a set of candidates with their intrinsic widths, and a declaration of how wide the image will be laid out. The browser combines them into a density for each candidate and chooses one. The src attribute may sit alongside and still lose.
The selection is not a lookup that always returns the same answer for the same viewport. The specification defines the candidate set, the source size, and the density computation precisely, then leaves the final choice to the browser with constraints. That division is the reason two browsers on identical hardware and identical window sizes can fetch different files.
One candidate, one descriptor
The srcset value is a comma-separated list. Each entry is a URL followed optionally by a single descriptor, either <number>w or <number>x. Whitespace around tokens is insignificant, and a trailing comma on a URL is stripped rather than treated as a separator.
The two descriptor types are not interchangeable. A width descriptor states the intrinsic width of the image resource in pixels. A density descriptor states how many device pixels the resource provides per CSS pixel. A single source set must use one type throughout; mixing 400w and 2x in one attribute is a conformance error. The parsing algorithm still accepts each candidate, and normalising the densities converts the width descriptors using the source size, so the mixed set has a defined meaning even though it is invalid.
A candidate with no descriptor is treated as 1x. A width descriptor of 0w is invalid, as is a negative density; 0x parses but implies infinite natural dimensions, which engines bound in practice. Widths must be positive integers, while density descriptors accept decimals.
src participates in the selection only under specific conditions. When the set uses density descriptors, src is treated as a 1x candidate. When the set uses width descriptors, src does not participate in selection at all: it remains the fallback for an engine that ignores srcset, and it is what a browser without support would load, but it is not a candidate in the computed set.
Commas inside URLs are a recurring point of confusion. The parser collects a URL up to the next whitespace, so a comma inside the URL — as in a data URL after the media type — belongs to the URL rather than to the list. The real hazard is different: an entry with no descriptor must be followed by whitespace before the next entry, and a URL that ends with a comma has those trailing commas stripped during parsing.
Density descriptors and the device pixel ratio
A 2x candidate is intended for a display with a device pixel ratio of 2. The browser knows the ratio for the window, and a candidate whose density matches or slightly exceeds it is preferred over one that falls short, because a lower density candidate is upscaled and loses detail.
Matching exactly is not required, and the browser does not always choose the smallest sufficient candidate. Two constraints push it in opposite directions. The output device has a fixed number of device pixels per CSS pixel, but the image may be rendered at a different size than the density calculation assumed, and the choice may be adjusted for network conditions, battery state, or a data-saver preference. The specification permits the browser to select any candidate from the set, and reserves the “selected source” only after the choice is made. currentSrc reports the URL that was chosen, which is the direct way to learn what happened.
The observable consequence is that 1x and 2x are hints, not guarantees. Code that infers the display’s pixel ratio from the loaded candidate is wrong, and code that assumes a 2x display always receives the 2x file is wrong too: the browser may return the 1x file from cache rather than fetch the larger one.
Width descriptors need a source size
A width descriptor has no meaning until the layout width is known. sizes supplies it as a source size list: comma-separated entries, each a media condition followed by a CSS length, terminating in an entry with no media condition that acts as the default.
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
sizes="(min-width: 70em) 60vw, (min-width: 40em) 90vw, 100vw"
width="1600" height="900" alt="">
Resolution walks the list in order and takes the first entry whose media condition evaluates to true. vw resolves against the viewport width, and the initial containing block it measures conventionally includes the space taken by a scrollbar, which is why 100vw can be wider than the visible area. Percentages are not permitted in sizes. The result is the source size in CSS pixels.
For each candidate, the browser computes an effective density by dividing the candidate’s width by the source size. With a viewport of 1000 CSS pixels and the first matching condition giving 90vw, the source size is 900, so the 800w candidate is about 0.89x, the 1600w candidate about 1.78x. On a ratio-2 display nothing in the set reaches 2x, so the browser typically selects the closest available candidate, the 1600w resource at 1.78x, and upscales it slightly. This is why width sets usually include a candidate at roughly twice the largest expected layout width.
sizes has no effect when the set uses density descriptors. When it is absent and the set uses width descriptors, the source size defaults to 100vw, so a 400w image in a narrow sidebar is measured against the full viewport width instead of the box it occupies.
The mismatch to avoid is between the sizes declaration and the actual rendered width. CSS can override the width of the image after selection, and the browser does not re-run selection when it does. A sizes value that declares 50vw while the stylesheet constrains the image to 300 pixels produces a candidate chosen for a much larger box, which wastes bytes and can delay the largest contentful paint for no visual benefit.
The picture element and ordered sources
picture is the art-direction layer. It contains zero or more source elements followed by exactly one img. Each source may carry media, type, srcset, and sizes. The browser evaluates the sources in order and takes the first one that matches: a media condition that evaluates true, and a type whose MIME type it supports. The chosen source’s srcset and sizes then drive selection exactly as they would on an img, and the resulting URL is loaded into the img element.
Art direction is the reason to use it. A wide crop and a narrow crop are different images, not different resolutions of one image, and no width descriptor can express that. The img inside remains the element that carries the embedded content semantics, the alternative text, and the intrinsic dimensions; a source has no fallback of its own. Format negotiation is the second reason: a source with type="image/avif" before one with type="image/webp" lets the browser skip formats it cannot decode, with the img providing the universally supported fallback.
Because selection happens before layout for the preload scanner, the media conditions in picture are evaluated against a viewport that is known, while the resulting layout width may not be. picture therefore works best when the choice depends on the viewport alone, and width-based selection works best when it depends on the rendered box.
Where the fetch starts, and what defers it
The preload scanner reads the markup ahead of the parser and starts image fetches before layout. That is why width and height attributes on the img matter for layout stability even though they do not affect selection: they let the browser reserve the box while the bytes arrive. The aspect-ratio derived from them applies before the image loads.
loading="lazy" defers the fetch until the image approaches the viewport, and it interacts with selection because the layout width may change between the time the element is parsed and the time the fetch starts. A lazy image whose sizes was written for the initial viewport can be selected against a later width. The sizes value auto exists for this case in engines that support it, allowing the browser to use the rendered width rather than the declared estimate; it is not universally available, and the fallback is the declared list.
A link rel="preload" for an image needs imagesrcset and imagesizes, with or without href, to participate in the same selection. Without them, a preload fetches one candidate that the img may not select, producing a duplicate download.
Checking what the browser did
currentSrc on the img reports the URL of the selected candidate after selection, and it is empty before selection. naturalWidth reports the intrinsic width of the loaded resource, which reveals whether the candidate matched the descriptor. Comparing currentSrc across two viewport widths and two device pixel ratios is the direct way to verify that a sizes list describes the layout: if the selected candidate does not change when the layout width crosses a breakpoint, the media condition is wrong or the attribute is being ignored.
The attributes describe a set, and the browser picks from it. Everything else — caching, priority, the decoder, the number of bytes on the wire — is outside the markup. What the markup controls is whether the right candidates exist and whether the size declaration matches the box they will be drawn into, and those are the two mistakes that no amount of network tuning can correct. The parsing rules that treat srcset as an attribute value rather than as markup are the same attribute rules that apply everywhere: a value has no structure beyond what its own grammar defines, and errors inside it are resolved by the element’s algorithm rather than by the tree builder.
References
- HTML Standard — Images — WHATWG
- HTML Standard — The img element — WHATWG
- MDN — The Image Embed element — MDN Web Docs