UI/UX Atlas
Responsive & Platform Intermediate

Responsive Images: srcset, picture & Modern Formats

Serving the right image at the right size and format is one of the highest-leverage performance and UX decisions a front-end team can make.

8 min read

The full lesson

Images make up more than half of all bytes transferred on the average web page. Get them wrong and you inflict a measurable layout-shift penalty, push your Largest Contentful Paint (LCP) past the 2.5-second threshold, and waste real money for users on metered data plans. Get them right and you win on performance, accessibility, and cost at the same time — which is why responsive image implementation is one of the highest-ROI decisions a front-end team can make.

Why a Single Image Source Is Never Enough

A 1400-pixel hero photo looks great on a desktop Retina display. Served unchanged to a 375-pixel phone on a cellular connection, it transfers four to eight times more pixels than the screen can show. The browser still has to decode and rasterize the entire bitmap, burning battery and memory in the process.

Pixel density multiplies the problem. A 2× device at 390 CSS pixels needs a 780-pixel-wide source to look sharp. That same file served to a 1× laptop at 1280 pixels is undersized and blurry. No single image satisfies every viewport at both 1× and 2× density without either wasting bandwidth or looking soft.

The responsive images spec — srcset, sizes, and the picture element — solves these two problems with two distinct tools. Mixing them up is the most common implementation mistake.

Resolution Switching with srcset and sizes

Resolution switching serves the same image at different widths. Use it when you want the browser to pick the most efficient source. You are not changing the crop or subject — just the file dimensions.

<img
  src="hero-800.webp"
  srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1600.webp 1600w"
  sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
  alt="Aerial view of a coastal city at golden hour"
  width="800"
  height="533"
/>

Breaking this down:

  • srcset with w descriptors tells the browser the intrinsic pixel width of each candidate file. Never use x descriptors here — they ignore sizes entirely and choose based on pixel density alone, ignoring viewport width.
  • sizes tells the browser how wide the image will render at various viewport widths, evaluated left to right. The final value (800px) is the default. Match these values to your actual CSS layout — if the image spans calc(100vw - 2rem) on mobile, say so.
  • src is the fallback for browsers that do not understand srcset. Put a mid-range source here.
  • width and height attributes are mandatory. Without them the browser cannot reserve layout space before the image loads, which causes cumulative layout shift (CLS). Set them to the intrinsic dimensions of the src image — CSS can scale it freely while the reserved slot holds.

The browser’s selection algorithm weighs viewport width, device pixel ratio, and — in modern implementations — network quality from the Network Information API. You supply the candidates; the browser chooses.

Art Direction with the picture Element

Art direction is a different problem. You want to serve a fundamentally different crop or composition depending on viewport size. The picture element gives the browser conditional source slots with full media-query control.

<picture>
  <source
    media="(max-width: 599px)"
    srcset="hero-portrait-400.webp 400w, hero-portrait-800.webp 800w"
    sizes="100vw"
    type="image/webp"
  />
  <source
    media="(min-width: 600px)"
    srcset="hero-landscape-800.webp 800w, hero-landscape-1600.webp 1600w"
    sizes="(max-width: 1200px) 50vw, 800px"
    type="image/webp"
  />
  <img
    src="hero-landscape-800.jpg"
    alt="Aerial view of a coastal city at golden hour"
    width="800"
    height="533"
  />
</picture>

Key rules:

  • The img inside picture is not optional. It is the fallback, carries the alt text, and holds the width/height attributes. Screen readers announce the img alt, not the picture wrapper.
  • source elements are evaluated top to bottom. The first match wins. Order from most restrictive to least.
  • Each source can carry its own srcset and sizes for resolution switching within an art-directed crop.
  • Use picture only when the composition genuinely changes. Do not use it just for format selection — that is what the type attribute is for.

Format Selection: AVIF, WebP, and the JPEG/PNG Fallback

Modern image formats deliver dramatically smaller files at the same visual quality:

FormatBrowser support (2026)Typical savings vs. JPEGBest for
AVIF96 %+ (all evergreen)40–60 %Photos, gradients, HDR
WebP97 %+25–35 %Photos, illustrations, transparency
JPEG XLExperimental (flag-gated)35–50 %Archival, high-fidelity photos
PNGUniversalLossless screenshots, pixel art
JPEGUniversalbaselineFallback for all photos

The standard pattern is AVIF first, WebP second, JPEG/PNG fallback — all inside a picture element:

<picture>
  <source type="image/avif" srcset="photo-800.avif 800w, photo-1600.avif 1600w" sizes="..." />
  <source type="image/webp" srcset="photo-800.webp 800w, photo-1600.webp 1600w" sizes="..." />
  <img src="photo-800.jpg" srcset="photo-800.jpg 800w, photo-1600.jpg 1600w" sizes="..." alt="..." width="800" height="533" />
</picture>

The browser picks the first source whose type it supports. A browser that does not support AVIF skips to WebP. One that does not support WebP falls back to the img JPEG chain.

Lazy Loading and Fetch Priority

Responsive images interact directly with Core Web Vitals through two HTML attributes.

loading="lazy" defers off-screen image fetches until the user scrolls near them. Apply it to every image that is not visible in the initial viewport. All modern browsers support it, and it degrades gracefully.

fetchpriority="high" tells the browser to fetch this image early in the preload queue. Apply it to the image most likely to be your Largest Contentful Paint element — the hero image, the first product photo, the above-the-fold illustration. Do not apply it to multiple images or the hint loses meaning.

<!-- LCP image: prioritize, never lazy-load -->
<img
  src="hero.webp"
  srcset="hero-800.webp 800w, hero-1600.webp 1600w"
  sizes="100vw"
  fetchpriority="high"
  alt="..."
  width="1600"
  height="900"
/>

<!-- Below-fold image: lazy-load -->
<img
  src="card-thumb.webp"
  srcset="card-thumb-400.webp 400w, card-thumb-800.webp 800w"
  sizes="(max-width: 600px) 50vw, 25vw"
  loading="lazy"
  alt="..."
  width="400"
  height="300"
/>

A common mistake is adding loading="lazy" to the LCP image on the assumption that lazy loading is always good. It is not. It delays the most important image and can push LCP past 4 seconds on slower connections.

Decoding, Aspect Ratio, and Layout Stability

decoding="async" tells the browser it can decode the image off the main thread, reducing jank. It is safe to apply universally and never delays rendering.

Aspect ratio reservation matters more than most teams realize. When width and height attributes are present, the browser uses them to calculate an intrinsic aspect ratio and reserves the exact layout slot before the image loads. This eliminates CLS from image loading entirely — one of the most impactful CLS fixes available for image-heavy pages.

If your image is CSS-scaled (width: 100%; height: auto), the browser still uses the width/height attributes to compute the ratio. You do not need to set a CSS aspect-ratio property unless you need to override the intrinsic ratio.

The Modern Build Pipeline Approach

Hand-authoring srcset strings for dozens of images is error-prone and hard to maintain. In 2026, the right approach is to treat image optimization as an automated build step.

  • Astroimport { Image } from 'astro:assets' generates AVIF + WebP variants, writes the correct srcset/sizes, and enforces alt at build time. Zero hand-authoring required.
  • Next.jsnext/image handles format negotiation server-side via the image optimization API, automatically serving AVIF or WebP based on the Accept header.
  • CDN-based — Cloudinary, imgix, and Cloudflare Images accept a single source file and serve the optimal format and width via URL parameters, deferring all transcoding to the edge.

For static sites not using a framework, a sharp-based build script (a Vite plugin, an Eleventy transform, or a standalone script) can generate all variants from a source directory. The key point: no human should ever be hand-resizing images or manually writing srcset strings.

Do

Set width and height attributes on every img element to reserve layout space and prevent CLS. Use fetchpriority=“high” on the LCP image only. Use AVIF as the leading source in your picture format waterfall. Generate all image variants in a build pipeline rather than manually.

Don't

Add loading=“lazy” to your LCP hero image — it delays the most critical render. Use pixel-density x descriptors with srcset when width w descriptors exist — x descriptors ignore viewport width and sizes entirely. Use picture just for format switching without art direction — that adds markup complexity for no gain over srcset plus type on source elements. Serve a 1400px wide image to a 375px mobile screen because ‘retina requires it’ — a 2x mobile screen only needs 750px.

Accessibility Considerations

Responsive images have two accessibility responsibilities.

Alt text belongs on the img element, always — not on picture or source. Write descriptive alt text for meaningful images. For decorative images, set alt="" so screen readers skip them. Never omit the attribute. An img without alt is announced by VoiceOver and NVDA as the filename or URL, which is meaningless to most users.

Color and contrast in images become relevant for text-over-image compositions. If you overlay text on a hero image, ensure sufficient contrast at every art-directed crop. A tightly cropped mobile version that shifts to a lighter background region may suddenly drop below the WCAG 2.2 AA threshold — 4.5:1 for normal text, 3:1 for large text. Test overlays at every responsive breakpoint, not just the desktop version.

Putting It Together: A Decision Tree

When implementing a responsive image, ask these questions in order:

  1. Does the crop or subject change at different viewports? Use picture with media on source elements for art direction.
  2. Do you need multiple formats (AVIF, WebP, fallback)? Add type to source elements inside picture.
  3. Is this a resolution switch only (same crop, different size)? Use img with srcset (w descriptors) and sizes.
  4. Is this the LCP image? Add fetchpriority="high" and omit loading="lazy".
  5. Is this below the fold? Add loading="lazy" and decoding="async".
  6. Does the img have width and height? If not, add them.

This sequence keeps markup minimal. Most images need only srcset, sizes, width, height, and the right loading attributes — not a full picture wrapper.