Skip to content

Responsive Images

Modern HTML provides <picture>, srcset, and sizes attributes for serving optimized images based on device capabilities.

<img src="hero-large.jpg" alt="Hero image">
<style>
@media (max-width: 768px) {
img { content: url('hero-small.jpg'); }
}
</style>

Or with JavaScript:

<img id="hero" alt="Hero image">
<script>
const img = document.getElementById('hero');
img.src = window.innerWidth < 768 ? 'hero-small.jpg' : 'hero-large.jpg';
</script>

Problems:

  • CSS content swap has poor support
  • JavaScript solution delays image loading
  • No consideration for pixel density
  • Browser downloads wrong image before resize

Serve different resolutions of the same image:

<img
src="hero-800.jpg"
srcset="
hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w,
hero-1600.jpg 1600w
"
sizes="(max-width: 600px) 100vw, 50vw"
alt="Hero image"
>

The browser chooses the best image based on viewport width and pixel density.

Serve different crops or aspect ratios:

<picture>
<source
media="(max-width: 600px)"
srcset="hero-portrait.jpg"
>
<source
media="(max-width: 1200px)"
srcset="hero-square.jpg"
>
<img src="hero-landscape.jpg" alt="Hero image">
</picture>

Serve modern formats with fallbacks:

<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero image">
</picture>
<img
srcset="
small.jpg 400w,
medium.jpg 800w,
large.jpg 1200w
"
sizes="(max-width: 600px) 100vw, 800px"
src="medium.jpg"
alt="Description"
>
  • 400w means the image is 400 pixels wide
  • Browser calculates which image to fetch based on sizes
<img
srcset="
logo.png 1x,
logo@2x.png 2x,
logo@3x.png 3x
"
src="logo.png"
alt="Logo"
>
  • 2x means for 2x pixel density displays (Retina)
  • Simpler but less flexible than width descriptors

Tells the browser how wide the image will be displayed:

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

Read as: “At 600px viewport or less, image is 100% of viewport width. At 1200px or less, 50%. Otherwise, 800px.”

<picture>
<!-- AVIF for browsers that support it -->
<source
type="image/avif"
srcset="
hero-400.avif 400w,
hero-800.avif 800w,
hero-1200.avif 1200w
"
sizes="(max-width: 600px) 100vw, 50vw"
>
<!-- WebP fallback -->
<source
type="image/webp"
srcset="
hero-400.webp 400w,
hero-800.webp 800w,
hero-1200.webp 1200w
"
sizes="(max-width: 600px) 100vw, 50vw"
>
<!-- JPEG fallback -->
<img
src="hero-800.jpg"
srcset="
hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w
"
sizes="(max-width: 600px) 100vw, 50vw"
alt="Hero image"
loading="lazy"
decoding="async"
>
</picture>
  • srcset and sizes: All modern browsers
  • <picture>: All modern browsers
  • AVIF: Chrome 85+, Firefox 93+, Safari 16.4+
  • WebP: All modern browsers
  • Always include meaningful alt text on the <img> element
  • alt goes on <img>, not on <source>
  • Decorative images should use alt=""
  • Art direction changes should not change the meaning
<img
src="hero.jpg"
srcset="..."
sizes="..."
alt="..."
loading="lazy"
decoding="async"
fetchpriority="high"
>
  • loading="lazy" - Defer offscreen images
  • decoding="async" - Don’t block rendering
  • fetchpriority="high" - Prioritize above-fold images

Use srcset for:

  • Same image at different resolutions
  • Retina/HiDPI display support
  • Bandwidth optimization

Use picture for:

  • Different crops at different breakpoints (art direction)
  • Serving modern formats with fallbacks
  • Different images for dark/light mode (with media queries)

Keep it simple when:

  • Image is small and doesn’t need optimization
  • Image is decorative and can be CSS background
  • Only one size is ever needed