Picture Element & Responsive Images
Responsive images are one of the most impactful performance optimizations you can make for the modern web. Users visit your site on devices ranging from 4K desktop monitors to budget smartphones on cellular connections. Serving the same image to every device wastes bandwidth on small screens and delivers blurry, undersized images on large displays.
The <picture> element, combined with the srcset and sizes attributes on <img>, gives you three powerful capabilities:
Resolution Switching
Serve different image sizes based on the device viewport width and pixel density. A phone loads a 400px-wide image; a 4K monitor loads a 1600px-wide image — from the same HTML.
Art Direction
Serve entirely different image crops per breakpoint. A wide landscape hero on desktop becomes a tight, face-focused crop on mobile — the <picture> element swaps the entire image source.
Format Selection
Deliver modern formats like AVIF or WebP to browsers that support them, while falling back to JPEG or PNG for older browsers — all without JavaScript.
note
The srcset attribute lets you declare multiple image candidates that the browser can choose from. Each candidate is described with either a width descriptor (w) or a pixel density descriptor (x).
Width Descriptors (w)
Width descriptors tell the browser the intrinsic width of each image candidate. The browser uses this information together with the sizes attribute to determine which source to load.
| 1 | <img |
| 2 | src="hero-800.jpg" |
| 3 | srcset="hero-400.jpg 400w, |
| 4 | hero-800.jpg 800w, |
| 5 | hero-1200.jpg 1200w, |
| 6 | hero-1600.jpg 1600w" |
| 7 | sizes="(max-width: 600px) 100vw, |
| 8 | (max-width: 1200px) 50vw, |
| 9 | 800px" |
| 10 | alt="Mountain landscape at sunset" |
| 11 | loading="lazy" |
| 12 | width="1600" |
| 13 | height="900" |
| 14 | /> |
Pixel Density Descriptors (x)
Pixel density descriptors tell the browser the device pixel ratio each image is intended for. Use this when the image is always displayed at the same CSS size but you want to provide sharper versions for high-density screens.
| 1 | <img |
| 2 | src="logo.png" |
| 3 | srcset="logo.png 1x, |
| 4 | logo@2x.png 2x, |
| 5 | logo@3x.png 3x" |
| 6 | alt="Company logo" |
| 7 | width="200" |
| 8 | height="60" |
| 9 | /> |
How the Browser Chooses
When using width descriptors, the browser:
- Reads the sizes attribute to determine the image's layout width at the current viewport
- Multiplies the layout width by the device pixel ratio to get the needed pixel width
- Selects the smallest candidate that is greater than or equal to the needed width
- If no candidate is large enough, selects the largest available candidate
warning
Complete example with multiple breakpoints for a typical layout:
| 1 | <!-- Full-width on mobile, 50% on tablet, 33% on desktop --> |
| 2 | <img |
| 3 | src="product-600.jpg" |
| 4 | srcset="product-400.jpg 400w, |
| 5 | product-600.jpg 600w, |
| 6 | product-800.jpg 800w, |
| 7 | product-1200.jpg 1200w" |
| 8 | sizes="(max-width: 640px) 100vw, |
| 9 | (max-width: 1024px) 50vw, |
| 10 | 33vw" |
| 11 | alt="Product photo" |
| 12 | width="1200" |
| 13 | height="800" |
| 14 | loading="lazy" |
| 15 | decoding="async" |
| 16 | /> |
The sizes attribute tells the browser how wide the image will be displayed in the layout — not the viewport size, but the actual rendered width of the image element. Without sizes, the browser assumes the image is 100vw (full viewport width).
Syntax
| 1 | sizes="(media-condition) length, |
| 2 | (media-condition) length, |
| 3 | fallback-length" |
Layout Scenarios
| 1 | <!-- Full-width hero image --> |
| 2 | <img |
| 3 | srcset="hero-800.jpg 800w, hero-1600.jpg 1600w" |
| 4 | sizes="100vw" |
| 5 | src="hero-800.jpg" |
| 6 | alt="Hero banner" |
| 7 | /> |
| 8 | |
| 9 | <!-- Sidebar image: always 300px wide --> |
| 10 | <img |
| 11 | srcset="avatar-300.jpg 300w, avatar-600.jpg 600w" |
| 12 | sizes="300px" |
| 13 | src="avatar-300.jpg" |
| 14 | alt="Author avatar" |
| 15 | /> |
| 16 | |
| 17 | <!-- Grid thumbnail: varies by viewport --> |
| 18 | <img |
| 19 | srcset="thumb-300.jpg 300w, thumb-600.jpg 600w" |
| 20 | sizes="(max-width: 768px) 50vw, |
| 21 | (max-width: 1200px) 33vw, |
| 22 | 25vw" |
| 23 | src="thumb-300.jpg" |
| 24 | alt="Gallery thumbnail" |
| 25 | /> |
The Math Behind Source Selection
The browser performs this calculation for each candidate in srcset:
1. Determine layout width from sizes:
e.g. (max-width: 768px) 50vw on a 1000px viewport = 500px
2. Multiply by device pixel ratio:
500px x 2 (Retina) = 1000px needed
3. Select smallest candidate >= needed:
srcset: 400w (too small), 800w (too small), 1200w ✓
info
The <picture> element wraps zero or more <source> elements and one <img> element. It provides art direction and format selection — two things that srcset alone cannot do.
Format Selection with type
The type attribute on <source> lets the browser skip sources with unsupported MIME types. This avoids downloading a file only to discover it is not supported.
| 1 | <picture> |
| 2 | <source srcset="photo.avif" type="image/avif" /> |
| 3 | <source srcset="photo.webp" type="image/webp" /> |
| 4 | <img |
| 5 | src="photo.jpg" |
| 6 | alt="Beach sunset photo" |
| 7 | width="1200" |
| 8 | height="800" |
| 9 | loading="lazy" |
| 10 | /> |
| 11 | </picture> |
Art Direction with media
Use the media attribute on <source> to swap entirely different images at different breakpoints. The browser uses the first <source> whose media condition matches.
| 1 | <picture> |
| 2 | <!-- Desktop: wide landscape crop --> |
| 3 | <source |
| 4 | media="(min-width: 1024px)" |
| 5 | srcset="hero-landscape.avif" |
| 6 | type="image/avif" |
| 7 | /> |
| 8 | <!-- Tablet: medium crop with context --> |
| 9 | <source |
| 10 | media="(min-width: 640px)" |
| 11 | srcset="hero-tablet.avif" |
| 12 | type="image/avif" |
| 13 | /> |
| 14 | <!-- Mobile: tight portrait crop --> |
| 15 | <source |
| 16 | srcset="hero-mobile.avif" |
| 17 | type="image/avif" |
| 18 | /> |
| 19 | <!-- Fallback --> |
| 20 | <img |
| 21 | src="hero-landscape.jpg" |
| 22 | alt="Mountain landscape" |
| 23 | width="1200" |
| 24 | height="600" |
| 25 | /> |
| 26 | </picture> |
info
When to Use picture vs srcset Alone
| Scenario | Use | Why |
|---|---|---|
| Same image, different sizes | srcset + sizes | Browser handles resolution switching natively |
| Different crops per breakpoint | <picture> + media | Art direction requires full source swap |
| Format negotiation (AVIF/WebP) | <picture> + type | type attribute lets browser skip unsupported formats |
| Both art direction + format | <picture> + media + type | Combine both in one element |
Art direction means showing a different visual composition at different screen sizes. A landscape photo with a subject centered may look great on desktop, but on a 375px-wide phone, the subject becomes tiny and lost. Art direction solves this by using different image crops for each context.
Three-Breakpoint Example
Consider a hero image of a person speaking at a conference. Here is how art direction adapts the crop:
| 1 | <picture> |
| 2 | <!-- Desktop: full wide landscape — speaker + stage + crowd --> |
| 3 | <source |
| 4 | media="(min-width: 1024px)" |
| 5 | srcset="hero-desktop.avif 1200w" |
| 6 | type="image/avif" |
| 7 | /> |
| 8 | |
| 9 | <!-- Tablet: medium crop — speaker + stage, tighter --> |
| 10 | <source |
| 11 | media="(min-width: 640px)" |
| 12 | srcset="hero-tablet.avif 800w" |
| 13 | type="image/avif" |
| 14 | /> |
| 15 | |
| 16 | <!-- Mobile: tight portrait crop — just the speaker's face --> |
| 17 | <source srcset="hero-mobile.avif 400w" type="image/avif" /> |
| 18 | |
| 19 | <!-- Fallback: JPEG versions for browsers without AVIF --> |
| 20 | <source |
| 21 | media="(min-width: 1024px)" |
| 22 | srcset="hero-desktop.jpg 1200w" |
| 23 | /> |
| 24 | <source |
| 25 | media="(min-width: 640px)" |
| 26 | srcset="hero-tablet.jpg 800w" |
| 27 | /> |
| 28 | <img |
| 29 | src="hero-mobile.jpg" |
| 30 | alt="Keynote speaker presenting at the annual developer conference" |
| 31 | width="1200" |
| 32 | height="600" |
| 33 | loading="eager" |
| 34 | fetchpriority="high" |
| 35 | /> |
| 36 | </picture> |
best practice
Device pixel ratio (DPR) determines how many physical pixels map to one CSS pixel. A standard display has a DPR of 1, Apple Retina displays have a DPR of 2, and some high-end phones have a DPR of 3. Higher DPR means more physical pixels per CSS pixel — and sharper images if you serve higher-resolution sources.
| DPR | Physical Pixels per CSS Pixel | Needed Source Width for 400px Element | Bandwidth vs 1x |
|---|---|---|---|
| 1x | 1 | 400px | 1.0x |
| 2x | 4 (2x2) | 800px | 4.0x (pixels), ~2x (file size) |
| 3x | 9 (3x3) | 1200px | 9.0x (pixels), ~3x (file size) |
Serving a 2x image to a 1x display wastes approximately 75% of the pixel data. Resolution switching solves this by letting the browser select the appropriate source for each DPR.
The Formula
To size your srcset candidates correctly:
1. Identify your largest layout width: maxLayoutWidth
2. Account for the highest DPR you want to support: maxDPR (usually 2 or 3)
3. Largest source in srcset should be: maxLayoutWidth x maxDPR
4. Include sources for each breakpoint at each relevant DPR
| 1 | <!-- Image displayed at 100vw (full width) |
| 2 | Max viewport: 1440px |
| 3 | DPR: up to 3x |
| 4 | Largest source needed: 1440 * 3 = 4320w |
| 5 | |
| 6 | But 3x on 1440px is extreme — cap at 2x: |
| 7 | Largest source: 1440 * 2 = 2880w |
| 8 | --> |
| 9 | <img |
| 10 | src="photo-800.jpg" |
| 11 | srcset="photo-400.jpg 400w, |
| 12 | photo-600.jpg 600w, |
| 13 | photo-800.jpg 800w, |
| 14 | photo-1200.jpg 1200w, |
| 15 | photo-1600.jpg 1600w, |
| 16 | photo-2400.jpg 2400w" |
| 17 | sizes="100vw" |
| 18 | alt="Landscape photo" |
| 19 | width="1440" |
| 20 | height="900" |
| 21 | loading="lazy" |
| 22 | /> |
pro tip
Image format selection is one of the easiest wins for performance. Modern formats like AVIF and WebP offer dramatically better compression than JPEG, often producing 30-50% smaller files at the same visual quality. The <picture> element lets you serve these formats transparently.
| Format | Compression | Transparency | Browser Support | Best For |
|---|---|---|---|---|
| AVIF | Best (50%+ smaller than JPEG) | Yes | Chrome, Firefox, Safari 16.4+ | Photos, hero images |
| WebP | Good (25-35% smaller than JPEG) | Yes | 97%+ global support | All image types |
| JPEG | Baseline | No | 100% — universal | Fallback, legacy |
| PNG | Large (lossless) | Yes | 100% — universal | Logos, icons, transparency |
Complete Format Negotiation Chain
| 1 | <picture> |
| 2 | <!-- AVIF: best compression, newer support --> |
| 3 | <source |
| 4 | srcset="photo.avif" |
| 5 | type="image/avif" |
| 6 | /> |
| 7 | <!-- WebP: good compression, wide support --> |
| 8 | <source |
| 9 | srcset="photo.webp" |
| 10 | type="image/webp" |
| 11 | /> |
| 12 | <!-- JPEG: universal fallback --> |
| 13 | <img |
| 14 | src="photo.jpg" |
| 15 | alt="Descriptive alt text" |
| 16 | width="800" |
| 17 | height="600" |
| 18 | loading="lazy" |
| 19 | decoding="async" |
| 20 | /> |
| 21 | </picture> |
Combining Format + Resolution Switching
| 1 | <picture> |
| 2 | <!-- AVIF with resolution switching --> |
| 3 | <source |
| 4 | type="image/avif" |
| 5 | srcset="hero-400.avif 400w, |
| 6 | hero-800.avif 800w, |
| 7 | hero-1200.avif 1200w, |
| 8 | hero-1600.avif 1600w" |
| 9 | sizes="(max-width: 768px) 100vw, 50vw" |
| 10 | /> |
| 11 | <!-- WebP with resolution switching --> |
| 12 | <source |
| 13 | type="image/webp" |
| 14 | srcset="hero-400.webp 400w, |
| 15 | hero-800.webp 800w, |
| 16 | hero-1200.webp 1200w, |
| 17 | hero-1600.webp 1600w" |
| 18 | sizes="(max-width: 768px) 100vw, 50vw" |
| 19 | /> |
| 20 | <!-- JPEG fallback --> |
| 21 | <img |
| 22 | src="hero-800.jpg" |
| 23 | srcset="hero-400.jpg 400w, |
| 24 | hero-800.jpg 800w, |
| 25 | hero-1200.jpg 1200w" |
| 26 | sizes="(max-width: 768px) 100vw, 50vw" |
| 27 | alt="Hero image with format and resolution switching" |
| 28 | width="1600" |
| 29 | height="900" |
| 30 | /> |
| 31 | </picture> |
info
Lazy loading defers the download of off-screen images until the user scrolls near them. This dramatically reduces initial page load time and data usage, especially on image-heavy pages.
Native Lazy Loading
| 1 | <!-- Lazy load below-the-fold images --> |
| 2 | <img |
| 3 | src="photo.jpg" |
| 4 | alt="Below the fold photo" |
| 5 | loading="lazy" |
| 6 | decoding="async" |
| 7 | width="800" |
| 8 | height="600" |
| 9 | /> |
| 10 | |
| 11 | <!-- Eager load above-the-fold images (default behavior) --> |
| 12 | <img |
| 13 | src="hero.jpg" |
| 14 | alt="Above the fold hero" |
| 15 | loading="eager" |
| 16 | fetchpriority="high" |
| 17 | width="1600" |
| 18 | height="900" |
| 19 | /> |
Preloading Critical Images
For above-the-fold hero images, use fetchpriority="high" to tell the browser to prioritize this download. Combined with loading="eager", this ensures the most important image loads first.
| 1 | <!-- Hero image: highest priority --> |
| 2 | <link rel="preload" as="image" href="hero.avif" type="image/avif" /> |
| 3 | <img |
| 4 | src="hero.jpg" |
| 5 | srcset="hero-800.jpg 800w, hero-1200.jpg 1200w" |
| 6 | sizes="100vw" |
| 7 | alt="Hero banner" |
| 8 | fetchpriority="high" |
| 9 | width="1200" |
| 10 | height="600" |
| 11 | /> |
| 12 | |
| 13 | <!-- Below-the-fold: lazy load --> |
| 14 | <img |
| 15 | src="content.jpg" |
| 16 | loading="lazy" |
| 17 | decoding="async" |
| 18 | alt="Content image" |
| 19 | width="800" |
| 20 | height="600" |
| 21 | /> |
Custom Lazy Loading with Intersection Observer
For more control over when images load (e.g., loading 200px before they enter the viewport), use the Intersection Observer API:
| 1 | const observer = new IntersectionObserver( |
| 2 | (entries) => { |
| 3 | entries.forEach((entry) => { |
| 4 | if (entry.isIntersecting) { |
| 5 | const img = entry.target; |
| 6 | img.src = img.dataset.src; |
| 7 | img.srcset = img.dataset.srcset || ""; |
| 8 | observer.unobserve(img); |
| 9 | } |
| 10 | }); |
| 11 | }, |
| 12 | { rootMargin: "200px 0px" } // Load 200px before visible |
| 13 | ); |
| 14 | |
| 15 | document.querySelectorAll("img[data-src]").forEach((img) => { |
| 16 | observer.observe(img); |
| 17 | }); |
| 1 | <!-- Usage with Intersection Observer --> |
| 2 | <img |
| 3 | data-src="photo.jpg" |
| 4 | data-srcset="photo-400.jpg 400w, photo-800.jpg 800w" |
| 5 | alt="Lazy loaded with JS fallback" |
| 6 | width="800" |
| 7 | height="600" |
| 8 | class="lazy-placeholder" |
| 9 | /> |
warning
Proper image sizing prevents layout shift (CLS), ensures correct aspect ratios, and provides a smooth user experience. The key is combining intrinsic attributes (width/height on the element) with CSS sizing techniques.
Preventing Layout Shift
| 1 | <!-- Always include width and height — CSS can override the display size --> |
| 2 | <img |
| 3 | src="photo.jpg" |
| 4 | alt="Photo with dimensions" |
| 5 | width="800" |
| 6 | height="600" |
| 7 | loading="lazy" |
| 8 | /> |
| 9 | |
| 10 | <!-- CSS controls the rendered size, HTML prevents CLS --> |
| 11 | <style> |
| 12 | img { |
| 13 | width: 100%; |
| 14 | height: auto; /* Maintains aspect ratio from width/height */ |
| 15 | } |
| 16 | </style> |
CSS aspect-ratio
| 1 | <!-- Modern approach: CSS aspect-ratio --> |
| 2 | <style> |
| 3 | .responsive-img { |
| 4 | width: 100%; |
| 5 | height: auto; |
| 6 | aspect-ratio: 800 / 600; /* width / height */ |
| 7 | object-fit: cover; |
| 8 | } |
| 9 | </style> |
| 10 | |
| 11 | <img |
| 12 | src="photo.jpg" |
| 13 | alt="Photo with CSS aspect ratio" |
| 14 | class="responsive-img" |
| 15 | width="800" |
| 16 | height="600" |
| 17 | /> |
object-fit and object-position
| 1 | <style> |
| 2 | /* Cover: fills container, may crop */ |
| 3 | .img-cover { |
| 4 | width: 100%; |
| 5 | height: 300px; |
| 6 | object-fit: cover; |
| 7 | object-position: center 30%; /* Focus on top 30% */ |
| 8 | } |
| 9 | |
| 10 | /* Contain: fits inside container, may letterbox */ |
| 11 | .img-contain { |
| 12 | width: 100%; |
| 13 | height: 300px; |
| 14 | object-fit: contain; |
| 15 | background: #0d0d0d; |
| 16 | } |
| 17 | </style> |
| 18 | |
| 19 | <img src="photo.jpg" alt="Cover example" class="img-cover" /> |
| 20 | <img src="photo.jpg" alt="Contain example" class="img-contain" /> |
best practice
Use this checklist to audit your responsive image implementation. Each item directly impacts Core Web Vitals and user experience.
| Check | Impact | Tool |
|---|---|---|
| srcset + sizes on all content images | Bandwidth savings up to 70% | Manual audit |
| Modern format (AVIF/WebP) with fallback | 30-50% smaller files | Network tab |
| loading="lazy" on below-the-fold | Faster initial load | Lighthouse |
| fetchpriority="high" on hero image | Better LCP score | Lighthouse, WebPageTest |
| width + height attributes on all img | Zero CLS from images | Lighthouse CLS audit |
| descriptive alt text | Accessibility compliance | axe DevTools, Lighthouse |
| decoding="async" on lazy images | Non-blocking decode | Performance audit |
| No oversized images (2x max) | Bandwidth optimization | PageSpeed Insights |
Recommended tools for auditing responsive images:
These are the most frequent responsive image mistakes and how to fix them.
| Mistake | Problem | Fix |
|---|---|---|
| Missing sizes attribute | Browser assumes 100vw, loads oversized images | Always add sizes matching your layout |
| Using <picture> when srcset suffices | Unnecessary complexity, duplicate source lists | Use srcset for same-crop resolution switching |
| Missing <img> fallback in <picture> | No image displays; accessibility broken | Always include <img> as last child of <picture> |
| Same image for all devices | Mobile downloads desktop-sized file | Add srcset with multiple sizes + sizes attr |
| No width/height attributes | Cumulative Layout Shift (CLS) | Add intrinsic dimensions for aspect ratio |
| Lazy loading hero images | Delayed LCP, poor Core Web Vitals | Use loading="eager" + fetchpriority="high" |
| Mixing w and x in srcset | Entire srcset is ignored by browser | Use only w or only x per srcset |
| Missing alt text | Accessibility violation, poor SEO | Always add descriptive alt attribute |
| 1 | <!-- ❌ BAD: No sizes, no srcset, no lazy loading --> |
| 2 | <img src="huge-desktop-photo.jpg" alt="Photo" /> |
| 3 | |
| 4 | <!-- ✅ GOOD: Full responsive image setup --> |
| 5 | <img |
| 6 | src="photo-600.jpg" |
| 7 | srcset="photo-400.jpg 400w, |
| 8 | photo-600.jpg 600w, |
| 9 | photo-800.jpg 800w, |
| 10 | photo-1200.jpg 1200w" |
| 11 | sizes="(max-width: 640px) 100vw, |
| 12 | (max-width: 1024px) 50vw, |
| 13 | 33vw" |
| 14 | alt="Descriptive alt text" |
| 15 | width="1200" |
| 16 | height="800" |
| 17 | loading="lazy" |
| 18 | decoding="async" |
| 19 | /> |
This example combines every technique: art direction, format selection, resolution switching, lazy loading, and CLS prevention.
| 1 | <!-- Hero: art direction + format selection + eager load --> |
| 2 | <picture> |
| 3 | <source |
| 4 | media="(min-width: 1024px)" |
| 5 | type="image/avif" |
| 6 | srcset="hero-desktop-1200.avif 1200w, |
| 7 | hero-desktop-1600.avif 1600w" |
| 8 | sizes="100vw" |
| 9 | /> |
| 10 | <source |
| 11 | media="(min-width: 1024px)" |
| 12 | type="image/webp" |
| 13 | srcset="hero-desktop-1200.webp 1200w, |
| 14 | hero-desktop-1600.webp 1600w" |
| 15 | sizes="100vw" |
| 16 | /> |
| 17 | <source |
| 18 | media="(min-width: 1024px)" |
| 19 | srcset="hero-desktop-1200.jpg 1200w, |
| 20 | hero-desktop-1600.jpg 1600w" |
| 21 | sizes="100vw" |
| 22 | /> |
| 23 | <source |
| 24 | media="(min-width: 640px)" |
| 25 | type="image/avif" |
| 26 | srcset="hero-tablet-800.avif 800w, |
| 27 | hero-tablet-1200.avif 1200w" |
| 28 | sizes="100vw" |
| 29 | /> |
| 30 | <img |
| 31 | src="hero-mobile-800.jpg" |
| 32 | srcset="hero-mobile-400.jpg 400w, |
| 33 | hero-mobile-800.jpg 800w" |
| 34 | sizes="100vw" |
| 35 | alt="Developer conference keynote" |
| 36 | width="1600" |
| 37 | height="600" |
| 38 | loading="eager" |
| 39 | fetchpriority="high" |
| 40 | /> |
| 41 | </picture> |
| 42 | |
| 43 | <!-- Content images: format selection + resolution switching + lazy load --> |
| 44 | <picture> |
| 45 | <source |
| 46 | type="image/avif" |
| 47 | srcset="content-400.avif 400w, |
| 48 | content-800.avif 800w, |
| 49 | content-1200.avif 1200w" |
| 50 | sizes="(max-width: 768px) 100vw, 50vw" |
| 51 | /> |
| 52 | <source |
| 53 | type="image/webp" |
| 54 | srcset="content-400.webp 400w, |
| 55 | content-800.webp 800w, |
| 56 | content-1200.webp 1200w" |
| 57 | sizes="(max-width: 768px) 100vw, 50vw" |
| 58 | /> |
| 59 | <img |
| 60 | src="content-800.jpg" |
| 61 | srcset="content-400.jpg 400w, |
| 62 | content-800.jpg 800w, |
| 63 | content-1200.jpg 1200w" |
| 64 | sizes="(max-width: 768px) 100vw, 50vw" |
| 65 | alt="Content section image" |
| 66 | width="1200" |
| 67 | height="800" |
| 68 | loading="lazy" |
| 69 | decoding="async" |
| 70 | /> |
| 71 | </picture> |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.