Elementary Standards
HTMLCSSWeb StandardsSEOBlogStart reading
Home / Blog / Responsive Images: srcset, sizes, and the picture Element Explained
HTML

Responsive Images: srcset, sizes, and the picture Element Explained

One image file was never going to work for every screen — here's how the three HTML tools for responsive images actually fit together.

Ship one 4000px photo to every visitor and you're either blurring it on a laptop or making someone on a train wait eight seconds for a hero image they'll glance at once. A single <img> was built for a web where every screen was roughly the same size. That assumption broke a decade ago, and it's exactly why a professional web development team treats image markup as a performance surface, not an afterthought — the file you ship and the file the browser actually needs are rarely the same file.

TL;DR: srcset plus sizes let the browser pick the right-sized image file for the layout it's actually rendering. <picture> exists for two specific jobs — art direction (swapping crops per breakpoint) and format fallback (AVIF/WebP to JPEG) — not general responsiveness. Pair both with aspect-ratio to stop layout shift, and never lazy-load the image above the fold.

one srcset, three renditions

What srcset and sizes actually do

The srcset attribute on <img> doesn't pick an image — it gives the browser a menu of candidate files and lets the browser's own layout engine choose. The browser already knows the viewport size, the device pixel ratio, and (once you tell it) the rendered width of the image. That's more information than you have at write time, so handing the decision to the browser produces better results than hardcoding breakpoints in JavaScript.

Width descriptors — the common case

Most of the time you'll use width descriptors (400w, 800w, 1200w), which tell the browser the actual pixel width of each file:

<img
  src="photo-800.jpg"
  srcset="photo-400.jpg 400w,
          photo-800.jpg 800w,
          photo-1200.jpg 1200w,
          photo-2400.jpg 2400w"
  sizes="(min-width: 900px) 50vw, 100vw"
  alt="A developer reviewing a layout on two monitors">

The src attribute still matters here — it's the fallback for any browser too old to understand srcset at all, so never omit it.

Pixel-density descriptors — when to reach for them instead

Pixel-density descriptors (1x, 2x, 3x) are simpler but less flexible: they assume the image renders at a single fixed CSS size and only vary by device pixel ratio (DPR), the ratio of physical pixels to CSS pixels on a given screen. Use them for fixed-size UI elements like logos and icons, where the display size never changes; use width descriptors for anything that scales with the layout, which is most content imagery.

Why does the sizes attribute matter if srcset already lists widths?

Because srcset only lists candidate files — it never tells the browser how wide the image will actually render on the page. Without sizes, the browser has to guess, and its fallback guess is the full viewport width, which means a phone browser might select your largest file for an image that's actually rendering in a 300px sidebar. The sizes attribute is a set of CSS-length hints, evaluated in order, that tells the browser the rendered width at each condition: (min-width: 900px) 50vw, 100vw means "50% of viewport above 900px, full viewport below it." Get this value wrong — copy it from a different layout, say — and every width descriptor in srcset becomes useless, because the input feeding the selection logic is wrong.

When do you actually need picture?

<picture> gets reached for constantly as "the responsive image tag," but it solves exactly two problems that srcset and sizes can't: changing which image shows up (not just which size of the same image), and offering alternate file formats. If you just need the same photo at different resolutions, srcset alone is both simpler and correct.

Art direction example

Art direction means swapping the actual crop or composition at different breakpoints — a wide landscape shot on desktop, a tighter portrait crop on mobile where the wide version would shrink the subject to nothing:

<picture>
  <source media="(max-width: 600px)" srcset="hero-tight-crop.jpg">
  <source media="(min-width: 601px)" srcset="hero-wide-crop.jpg">
  <img src="hero-wide-crop.jpg" alt="Team standing in front of the office building">
</picture>

Format fallback example

Format fallback offers the same composition in progressively more-supported formats, letting the browser pick the first one it can decode — modern formats like AVIF and WebP compress better than JPEG, but you still need a universal fallback:

<picture>
  <source type="image/avif" srcset="photo.avif">
  <source type="image/webp" srcset="photo.webp">
  <img src="photo.jpg" alt="Close-up of a mechanical keyboard">
</picture>

The order matters: the browser uses the first <source> whose type it supports, falling through to the plain <img> if none match.

Picking breakpoints without overthinking it

A 400 / 800 / 1200 / 2400w ladder covers most layouts: 400w for thumbnails, 800w and 1200w for typical content-width images, 2400w as the 2x-DPR top end for a 1200px-wide display. Full-bleed heroes sometimes justify a 3200w step above that — but extra variants beyond what any real layout renders just add encode time without ever getting selected.

Stopping layout shift — aspect-ratio, width/height, and object-fit

None of the above helps if the browser doesn't know the image's dimensions before it loads, because that's what causes layout shift: the page renders with no space reserved, the image arrives, and everything below it jumps. The fix is to always set width and height attributes (even though the actual rendered size is responsive) — modern browsers use them purely to compute the correct aspect-ratio and reserve space, not to force a fixed pixel size.

img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

object-fit: cover is the piece people skip: once the box has a fixed aspect ratio, a source image that doesn't match it will squish unless you crop it to fill the box instead. Reserved space is what protects your Largest Contentful Paint (LCP) and layout shift (CLS) scores — the layout doesn't move once the real file decodes.

Lazy loading — and why the hero image is the one exception

Native lazy loading (loading="lazy") is almost always the right default: it defers off-screen images until the user scrolls near them, cutting initial page weight for free. The one image that should never carry it is your Largest Contentful Paint candidate — typically the hero or the first content image above the fold. Lazy-loading it deprioritizes the exact resource your LCP metric is timing — the single most common self-inflicted regression in this whole topic.

Quick decision table

A short reference for which tool actually applies:

I need to...Use this
Serve the same image at different resolutionssrcset + sizes on <img>
Show a different crop/composition per breakpoint<picture> with media on <source>
Offer AVIF/WebP with a JPEG fallback<picture> with type on <source>
Show one small, fixed-size image (icon, logo)Plain <img>, no responsive markup needed

Common mistakes

FAQ

Do I always need the sizes attribute if I use srcset?

Yes, unless the image always renders at exactly 100% of viewport width — otherwise omitting sizes makes the browser assume full width and pick an oversized file.

What's the difference between srcset width descriptors and pixel-density descriptors?

Width descriptors (800w) describe the file's pixel width and work with sizes for layouts that reflow; pixel-density descriptors (2x) assume one fixed display size, so they fit icons and logos, not content that resizes.

Does picture replace img?

No — <picture> is a wrapper that still requires an <img> inside it as the fallback; it adds <source> options in front, it doesn't replace it.

Why does my responsive image look squished?

Almost always a mismatched aspect-ratio without object-fit: cover — the box forces a ratio the source doesn't have, and without object-fit the browser stretches rather than crops.

Should the hero image ever be lazy-loaded?

No. The hero or LCP candidate should load eagerly; lazy-loading it delays the exact resource your Largest Contentful Paint metric measures.

Do I need to serve 3x images for high-DPI screens, or is that overkill?

A 2400w or 2x top-tier file already covers most 1200px-wide 2x displays; a dedicated 3x tier is usually only worth it for full-bleed hero imagery.

What happened to Client Hints — are they still relevant?

They still exist, but srcset/sizes handle the same problem without server-side negotiation, which is why most teams never implement Client Hints at all.

Once you separate the two jobs, none of this is complicated: srcset and sizes answer "which resolution," <picture> answers "which image or which format." Both are native HTML, both degrade gracefully, and neither needs a build step to keep working in ten years — the actual argument for using the markup the spec gives you instead of a client-side image component.

Back to the blog
More notes

Keep reading