NewProfiles are here!View user profiles guide
Back to Blog
Web DevelopmentSEOOptimization

Responsive Images with srcset and sizes — The Complete Developer Guide

December 8, 2025 7 min read 1 views

Serving a 4K hero image to a phone on 3G is wasteful and slow. Responsive images let you serve the right image size to every device. This guide covers srcset, sizes, the picture element, and real-world implementation patterns.

Quick Takeaways

  • The Core Problem
  • Method 1: srcset with Width Descriptors
  • How srcset Works
  • How sizes Works

A 2560px wide hero image looks gorgeous on a 27-inch monitor. On a 375px wide iPhone, that same image is loaded, decoded, and then displayed at 13% of its original resolution — wasting 87% of the pixels, the bandwidth to download them, and the CPU time to decode them.

Responsive images solve this by letting you provide multiple versions of an image and letting the browser choose the most appropriate one based on the device's screen size, pixel density, and network conditions.

The Core Problem

Without responsive images, you have two bad options:

  1. Serve the largest image (for retina displays): Desktop experience is great, but mobile users download 3-5x more data than they need.
  2. Serve a small image (for mobile): Fast on phones, but looks blurry and pixelated on high-resolution displays.

Responsive images give you the best of both: serve small images to small screens and large images to large screens.

Method 1: srcset with Width Descriptors

The srcset attribute provides the browser with multiple image sources and their widths:

<img
  src="photo-800.jpg"
  srcset="
    photo-400.jpg   400w,
    photo-800.jpg   800w,
    photo-1200.jpg  1200w,
    photo-1600.jpg  1600w,
    photo-2400.jpg  2400w
  "
  sizes="(max-width: 600px) 100vw,
         (max-width: 1200px) 50vw,
         800px"
  alt="A beautiful landscape photograph"
  width="1600"
  height="900"
  loading="lazy"
>

How srcset Works

The srcset attribute lists available image files with their intrinsic widths:

  • photo-400.jpg 400w — This file is 400 pixels wide
  • photo-800.jpg 800w — This file is 800 pixels wide
  • photo-2400.jpg 2400w — This file is 2400 pixels wide

The w descriptor tells the browser the actual pixel width of each file. The browser uses this information, combined with the sizes attribute and the device's pixel density, to choose the optimal file.

How sizes Works

The sizes attribute tells the browser how wide the image will be displayed at different viewport widths:

sizes="(max-width: 600px) 100vw,
       (max-width: 1200px) 50vw,
       800px"

Reading the conditions in order:

  • On viewports up to 600px wide: the image occupies 100% of the viewport width (100vw)
  • On viewports up to 1200px wide: the image occupies 50% of the viewport width (50vw)
  • On larger viewports: the image is displayed at a fixed 800px

The Browser's Selection Process

On a 375px wide iPhone with 2x pixel density:

  1. Browser checks sizes: viewport is 375px (under 600px), so image displays at 100vw = 375px
  2. Device pixel ratio is 2x, so the browser needs 375 × 2 = 750 CSS pixels worth of image data
  3. Browser picks photo-800.jpg (800w, closest to 750 without being too small)

On a 1440px wide desktop with 1x display:

  1. Browser checks sizes: viewport is 1440px (over 1200px), so image displays at 800px
  2. Device pixel ratio is 1x, so the browser needs 800 × 1 = 800 pixels
  3. Browser picks photo-800.jpg

On a 1440px wide MacBook with 2x Retina display:

  1. Image displays at 800px, but pixel ratio is 2x, so browser needs 1600 pixels
  2. Browser picks photo-1600.jpg

Method 2: srcset with Pixel Density Descriptors

For fixed-size images (logos, icons, avatars), use density descriptors instead:

<img
  src="logo-200.png"
  srcset="
    logo-200.png 1x,
    logo-400.png 2x,
    logo-600.png 3x
  "
  alt="Company Logo"
  width="200"
  height="50"
>

The 1x, 2x, 3x descriptors correspond directly to device pixel ratios:

  • Standard displays (1x DPR): Load logo-200.png
  • Retina/HiDPI displays (2x DPR): Load logo-400.png
  • Ultra-high-density displays (3x DPR): Load logo-600.png

This is simpler than width descriptors but only works when the image display size doesn't change with viewport width. For content images that resize responsively, use width descriptors and sizes.

Method 3: The picture Element (Art Direction)

When you need to serve completely different images (not just different sizes of the same image) based on viewport or format support, use the <picture> element:

Art Direction: Different Crops for Different Devices

<picture>
  <!-- Mobile: tight crop on the subject -->
  <source
    media="(max-width: 600px)"
    srcset="photo-mobile.jpg 600w"
    sizes="100vw"
  >

  <!-- Tablet: medium crop -->
  <source
    media="(max-width: 1024px)"
    srcset="photo-tablet.jpg 1024w"
    sizes="100vw"
  >

  <!-- Desktop: full wide shot -->
  <img
    src="photo-desktop.jpg"
    srcset="photo-desktop.jpg 1600w, photo-desktop-2x.jpg 3200w"
    sizes="100vw"
    alt="Product hero shot"
    width="1600"
    height="600"
  >
</picture>

Art direction is essential for hero images where the composition matters. A wide landscape shot looks great on desktop but the subject is tiny on mobile. A tighter crop for mobile keeps the subject prominent.

Format Negotiation: Serve Modern Formats

<picture>
  <!-- AVIF: smallest file, modern browsers -->
  <source
    type="image/avif"
    srcset="photo.avif 800w, photo-2x.avif 1600w"
    sizes="(max-width: 800px) 100vw, 800px"
  >

  <!-- WebP: good compression, wide support -->
  <source
    type="image/webp"
    srcset="photo.webp 800w, photo-2x.webp 1600w"
    sizes="(max-width: 800px) 100vw, 800px"
  >

  <!-- JPEG: universal fallback -->
  <img
    src="photo.jpg"
    srcset="photo.jpg 800w, photo-2x.jpg 1600w"
    sizes="(max-width: 800px) 100vw, 800px"
    alt="Product photo"
    width="800"
    height="600"
    loading="lazy"
  >
</picture>

Browser behavior: try AVIF first (if supported), then WebP, then fall back to JPEG. This serves the smallest possible file to every browser.

Generating Responsive Image Variants

Creating 5+ sizes of every image manually is tedious. Automate it:

ImageMagick (Command Line)

# Generate multiple sizes from one source image
for width in 400 800 1200 1600 2400; do
  magick source.jpg -resize ${width}x -quality 82 "photo-${width}.jpg"
done

Sharp (Node.js)

const sharp = require('sharp');

const widths = [400, 800, 1200, 1600, 2400];

for (const width of widths) {
  await sharp('source.jpg')
    .resize(width)
    .jpeg({ quality: 82 })
    .toFile(`photo-${width}.jpg`);

  // Also generate WebP
  await sharp('source.jpg')
    .resize(width)
    .webp({ quality: 80 })
    .toFile(`photo-${width}.webp`);
}

Next.js Image (Automatic)

If you're using Next.js, the next/image component generates responsive variants automatically:

import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1600}
  height={900}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px"
/>

Next.js automatically generates optimized variants at different widths and serves them in WebP/AVIF to supporting browsers. Zero manual configuration required.

Common Responsive Image Patterns

Full-Width Hero Image

<img
  src="hero-1200.jpg"
  srcset="hero-600.jpg 600w, hero-900.jpg 900w, hero-1200.jpg 1200w, hero-1800.jpg 1800w, hero-2400.jpg 2400w"
  sizes="100vw"
  alt="Hero banner"
  width="2400" height="800"
  fetchpriority="high"
>

Note: No loading="lazy" — hero images should load immediately. fetchpriority="high" tells the browser to prioritize this image.

Content Image (Article Body)

<img
  src="content-800.jpg"
  srcset="content-400.jpg 400w, content-800.jpg 800w, content-1200.jpg 1200w"
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 66vw, 800px"
  alt="Descriptive alt text"
  width="800" height="500"
  loading="lazy"
>

Grid/Card Thumbnail

<img
  src="thumb-400.jpg"
  srcset="thumb-200.jpg 200w, thumb-400.jpg 400w, thumb-600.jpg 600w"
  sizes="(max-width: 600px) 50vw, (max-width: 1200px) 33vw, 300px"
  alt="Grid item"
  width="400" height="300"
  loading="lazy"
>

Debugging Responsive Images

Which Image Did the Browser Choose?

  1. Open Chrome DevTools (F12)
  2. Right-click the image → Inspect
  3. In the Elements panel, the currentSrc property shows which image file the browser selected
  4. Or in the Console: document.querySelector('img').currentSrc

Testing Different Viewports

  1. Open DevTools → Toggle device toolbar (Ctrl+Shift+M)
  2. Select different devices (iPhone, iPad, desktop)
  3. Reload the page (responsive image selection happens at load time)
  4. Check currentSrc for each device size

Hosting Responsive Image Variants

You need your image variants to be fast and permanently accessible. Upload all your responsive variants to ImgLink to get direct links with CDN delivery. Use the direct URLs in your srcset attributes, and every variant loads fast from the CDN edge closest to your visitor.

For optimization before upload: use the ImgLink Image Resizer to create your width variants, the Image Compressor to optimize each one, and the Image Converter to generate WebP versions. Then batch upload all variants at once.

Apply This Workflow on ImgLink

ImgLink is built for the exact workflow covered in this guide: fast uploads, permanent direct links, Cloudflare CDN delivery, and no-signup sharing when you need to move quickly. If you want to turn the advice above into a repeatable publishing system, start with one canonical hosted image URL and reuse it across docs, posts, forums, and social channels.

Recommended Next Steps

Use these related resources to keep building the same workflow across adjacent image-hosting topics:

Need permanent image hosting?

Upload images with permanent direct links, fast CDN delivery, and no signup required. Use ImgLink for the workflows this guide discusses.

Comments