Responsive Images Explained: srcset, sizes and <picture>
By the PNGful team · Published July 13, 2026 · 9 min read
A phone on cellular data and a 5K monitor on fiber should not download the same image file — yet with a plain <img src> tag, they do. Responsive image markup fixes that by describing your available files to the browser and letting it pick the best one. The syntax looks cryptic at first, but it rests on three ideas that each take a minute to understand.
The problem responsive images solve
Every image file has an intrinsic size— its actual pixel dimensions, say 1600×900. The display sizeis whatever your CSS layout gives it: 100% of a 360 px phone screen, or a 700 px column on desktop.
When intrinsic size is much larger than display size, bandwidth is wasted; when it is smaller, the image looks blurry — especially on high-density screens, where a css-width of 360 px may map to 720 or 1080 device pixels. No single file is right for every visitor. The fix is to export several widths of the same image (for example 400, 800, 1200 and 1600 pixels wide, easy to produce with the resizer or in one pass with the website image optimizer) and describe them all in markup.
srcset and width descriptors
The srcset attribute lists your candidate files, each tagged with its intrinsic width using a w descriptor:
<img
src="/hero-800.jpg"
srcset="
/hero-400.jpg 400w,
/hero-800.jpg 800w,
/hero-1200.jpg 1200w,
/hero-1600.jpg 1600w"
alt="Sunrise over the harbor">400wmeans “this file is 400 pixels wide.” You are stating facts about your files, not giving instructions. The browser combines this list with the display width and the screen’s device-pixel ratio, then picks the smallest file that will still look sharp. A 2× phone displaying the image at 360 css-pixels needs about 720 device pixels, so it grabs hero-800.jpg; a desktop showing it at 700 px on a 1× monitor also picks the 800; the 1600 is reserved for large, dense screens. The plain src remains as a fallback for anything that does not understand srcset.
For the file set itself, a spread that roughly doubles — 400/800/1600, or 400/800/1200/1600 for finer steps — covers real devices well. More than five or six candidates adds markup without meaningful savings.
You may also see x descriptors (logo.png 1x, logo@2x.png 2x) in older tutorials. Those only account for screen density, not layout width, so they suit fixed-size images like logos. For anything fluid, wdescriptors are the right tool — they let the browser factor in both at once.
The sizes attribute
There is a catch: the browser chooses a file beforeit has parsed your CSS, so it does not yet know the display width. By default it assumes the image spans the full viewport. If your image actually sits in a 700 px column, that assumption over-downloads on every desktop visit.
The sizes attribute closes the gap by telling the browser, up front, how wide the image will render at various viewport widths:
sizes="(max-width: 740px) 100vw, 700px"Read it as: “below 740 px viewports the image spans the full width (100vw); otherwise it renders at 700 px.” The browser evaluates the conditions in order and uses the first match. The values are a promise about your layout, not measured from it — if you change the CSS, update sizes to match. It only needs to be approximately right: the browser is choosing between discrete files, so being off by a few percent changes nothing.
picture for format fallbacks
srcset handles multiple sizes; the <picture> element handles multiple formats. Each <source> advertises a MIME type, and the browser uses the first type it supports, falling through to the inner <img>:
<picture>
<source
type="image/avif"
srcset="/hero-800.avif 800w, /hero-1600.avif 1600w"
sizes="(max-width: 740px) 100vw, 700px">
<source
type="image/webp"
srcset="/hero-800.webp 800w, /hero-1600.webp 1600w"
sizes="(max-width: 740px) 100vw, 700px">
<img
src="/hero-800.jpg"
srcset="/hero-800.jpg 800w, /hero-1600.jpg 1600w"
sizes="(max-width: 740px) 100vw, 700px"
width="1600" height="900"
alt="Sunrise over the harbor">
</picture>Note that the <img>inside is not decorative — it is the element that actually renders, carries the alt text and dimensions, and serves browsers that support neither modern format. In 2026, WebP support is essentially universal, so a pragmatic setup is often WebP sources (convert PNGs here) with a well-compressed JPG fallback; add AVIF on top for your heaviest hero images, where its extra savings are largest.
<picture> also supports media attributes on sources for art direction— serving a tighter crop on small screens rather than a shrunken version of the wide shot. Use it sparingly; most images only need the format-and-size treatment.
width, height and layout shift
One attribute pair quietly outranks everything above for user experience: width and height. Set them to the image’s intrinsic dimensions (any one of the variants works — what matters is the ratio):
<img src="/hero-800.jpg" width="1600" height="900" alt="…">
/* CSS keeps it fluid: */
img { max-width: 100%; height: auto; }From these two numbers the browser computes the aspect ratio and reserves the correct space in the layout before the file downloads. Without them, content renders, then jumps down as each image arrives — the flicker measured by the Cumulative Layout Shift (CLS) metric in Google’s Core Web Vitals. The attributes do not fix the rendered size (your CSS still controls that); they only declare the ratio. It is the cheapest performance win in this entire article.
While you are in the tag: add loading="lazy" to below-the-fold images, and leave it off the primary above-the-fold image so it starts downloading immediately.
A complete example
Putting all the pieces together for a content image in a 700 px column — three widths, two formats, dimensions declared, lazy-loaded:
<picture>
<source
type="image/webp"
srcset="/photo-400.webp 400w, /photo-800.webp 800w, /photo-1400.webp 1400w"
sizes="(max-width: 740px) 100vw, 700px">
<img
src="/photo-800.jpg"
srcset="/photo-400.jpg 400w, /photo-800.jpg 800w, /photo-1400.jpg 1400w"
sizes="(max-width: 740px) 100vw, 700px"
width="1400" height="933"
loading="lazy"
alt="Fishing boats tied up at the dock at low tide">
</picture>Six files, one block of markup, and every visitor — phone or 5K desktop, WebP or not — gets an appropriately sized, sharp image with zero layout shift. Generating the variants is the tedious part, which is exactly what the website image optimizer automates: it exports the size-and-format matrix and hands you this markup, dimensions filled in, ready to paste.