
Responsive Images
- 92 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
responsive-images is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- responsive-images
- AI & Agent Building
- AI-coding skill
Responsive Images by the numbers
- 92 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #4,749 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill responsive-imagesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 92 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Responsive Images
Overview
Responsive images serve the right image size and format based on viewport, device pixel ratio, and browser capabilities. Proper implementation prevents layout shift (CLS), optimizes Largest Contentful Paint (LCP), and reduces bandwidth by 50-70% with modern formats.
When to use: Any page with images, especially content images, hero images, product photos, and gallery layouts.
When NOT to use: Inline SVG icons, CSS background patterns, or canvas-rendered graphics.
Quick Reference
| Pattern | Approach | Key Points |
|---|---|---|
| Responsive sizing | srcset with width descriptors (w) + sizes | Browser selects optimal image for viewport and DPR |
| Modern formats | <picture> with AVIF, WebP, JPEG sources | AVIF saves 70%, WebP saves 50% vs JPEG |
| Art direction | <picture> with media queries | Different crops per viewport |
| LCP hero image | loading="eager" + fetchpriority="high" | Prioritize download for Core Web Vitals |
| Below-fold images | loading="lazy" | Defer until near viewport |
| Prevent CLS | width + height attributes | Browser reserves space before load |
| Fixed containers | object-fit: cover or contain | Maintain aspect ratio in constrained space |
| Format fallback | AVIF, WebP, JPEG source order | Best compression first, universal fallback last |
Recommended Image Sizes
| Use Case | Widths to Generate | Sizes Attribute |
|---|---|---|
| Full-width hero | 800w, 1200w, 1600w, 2400w | 100vw |
| Content width | 400w, 800w, 1200w | (max-width: 768px) 100vw, 800px |
| Grid cards (3-col) | 300w, 600w, 900w | (max-width: 768px) 100vw, 33vw |
| Sidebar thumbnail | 150w, 300w | 150px |
Loading Strategy
| Image Position | loading | fetchpriority | Why |
|---|---|---|---|
| Hero/LCP | eager | high | Optimize LCP, prioritize download |
| Above fold (not LCP) | eager | omit | Load normally |
| Below fold | lazy | omit | Defer until near viewport |
| Off-screen carousel | lazy | omit | Defer until interaction |
Format Comparison
| Format | Quality | File Size | Browser Support | Use Case |
|---|---|---|---|---|
| JPEG | Good | Medium | 100% | Photos, complex images |
| PNG | Lossless | Large | 100% | Logos, transparency |
| WebP | Excellent | Small | 96%+ | Modern browsers, photos |
| AVIF | Excellent | Smallest | 93%+ | Newest format, fallback required |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
| Omitting width and height attributes on img elements | Always include width and height to prevent CLS layout shift |
| Lazy loading the LCP hero image | Use loading="eager" and fetchpriority="high" for LCP images |
| Using density descriptors (1x, 2x) for variable-width images | Use width descriptors (400w, 800w) with a sizes attribute |
| Missing alt text on content images | Provide descriptive alt text; use alt="" only for decorative images |
| Serving only JPEG without modern format fallbacks | Use <picture> with AVIF and WebP sources falling back to JPEG |
Delegation
- Audit a page for responsive image issues and CLS problems: Use
Exploreagent to scan HTML for missing attributes, incorrect loading strategies, and format gaps - Convert all images on a page to use picture element with modern formats: Use
Taskagent to rewrite img tags with AVIF/WebP/JPEG fallback chain - Plan an image optimization pipeline for a multi-page site: Use
Planagent to design srcset breakpoints, format conversion workflow, and CDN integration
References
- srcset, sizes, and width descriptor patterns
- Picture element and art direction
- Modern image formats: WebP, AVIF, and conversion tools
- Lazy loading, fetchpriority, and LCP optimization
- Aspect ratio, object-fit, and CLS prevention
Aspect Ratio and CLS Prevention
The Problem: Cumulative Layout Shift (CLS)
When images load without reserved space, content below shifts down, causing poor user experience and hurting Core Web Vitals scores.
Solution 1: Explicit Width and Height (Recommended)
Always include width and height attributes. Browsers use these to calculate aspect ratio and reserve space.
<img src="/image.jpg" alt="Image" width="800" height="600" loading="lazy" />Width/height don't constrain the image size -- they only set the aspect ratio. CSS can still make the image responsive:
img {
max-width: 100%;
height: auto;
}Or in Tailwind: class="w-full h-auto"
Solution 2: CSS aspect-ratio Property
Use when dimensions aren't known or for container-based layouts:
<!-- 16:9 aspect ratio -->
<div class="aspect-video">
<img src="/image.jpg" alt="Image" class="w-full h-full object-cover" />
</div>
<!-- 4:3 aspect ratio -->
<div class="aspect-4/3">
<img src="/image.jpg" alt="Image" class="w-full h-full object-cover" />
</div>
<!-- Square (1:1) -->
<div class="aspect-square">
<img src="/image.jpg" alt="Image" class="w-full h-full object-cover" />
</div>Custom ratios in CSS:
.aspect-ultrawide {
aspect-ratio: 21 / 9;
}
.aspect-photo {
aspect-ratio: 3 / 2;
}
.aspect-portrait {
aspect-ratio: 9 / 16;
}Common Aspect Ratios
| Ratio | CSS | Use Case |
|---|---|---|
| 16:9 | aspect-[16/9] | Video thumbnails, hero images |
| 4:3 | aspect-[4/3] | Standard photos, older displays |
| 3:2 | aspect-[3/2] | DSLR photos, 35mm film |
| 1:1 | aspect-square | Profile pictures, Instagram-style |
| 21:9 | aspect-[21/9] | Ultrawide banners, cinematic |
| 9:16 | aspect-[9/16] | Vertical video (TikTok, Stories) |
object-fit Property
| Value | Behavior | Use Case |
|---|---|---|
cover | Fill container, crop edges | Card images, backgrounds |
contain | Fit inside, preserve all content | Logos, product photos |
fill | Stretch to fill | Avoid unless necessary |
scale-down | Smaller of contain or original | Mixed content sizes |
When to Use Each
| Scenario | object-fit | Reasoning |
|---|---|---|
| Card images | cover | Fill space, crop unimportant edges |
| Product photos | contain | Show entire product |
| Profile pictures | cover | Fill circle/square |
| Logos | contain | Show entire logo |
| Hero backgrounds | cover | Fill viewport, no gaps |
object-position Property
Control which part of the image is visible when cropped with object-cover:
<img src="/portrait.jpg" alt="Portrait" class="object-cover object-top" />
<img src="/scene.jpg" alt="Scene" class="object-cover object-center" />
<img src="/product.jpg" alt="Product" class="object-cover object-bottom" />| Scenario | Position | Why |
|---|---|---|
| Portrait faces | object-top | Keep face visible when cropped |
| Landscape | object-center | Balance composition |
| Logo in corner | object-left-top | Keep branding visible |
Complete Pattern Examples
Card with Fixed Aspect Ratio
<div class="overflow-hidden rounded-lg">
<div class="aspect-video">
<img
src="/card-image-800.jpg"
srcset="
/card-image-400.jpg 400w,
/card-image-800.jpg 800w,
/card-image-1200.jpg 1200w
"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
alt="Card image"
width="800"
height="450"
loading="lazy"
class="w-full h-full object-cover"
/>
</div>
<div class="p-4">
<h3>Card Title</h3>
<p>Card content...</p>
</div>
</div>Profile Picture (Circle)
<div class="w-32 h-32 rounded-full overflow-hidden">
<img
src="/profile.jpg"
alt="Profile picture"
width="128"
height="128"
loading="lazy"
class="w-full h-full object-cover"
/>
</div>Hero with Text Overlay
<div class="aspect-21/9 relative">
<img
src="/hero-1600.jpg"
srcset="
/hero-800.jpg 800w,
/hero-1200.jpg 1200w,
/hero-1600.jpg 1600w,
/hero-2400.jpg 2400w
"
sizes="100vw"
alt="Hero image"
width="2400"
height="1028"
loading="eager"
fetchpriority="high"
class="w-full h-full object-cover"
/>
<div class="absolute inset-0 flex items-center justify-center">
<h1 class="text-white text-5xl font-bold">Hero Title</h1>
</div>
</div>Common Mistakes
<!-- Bad: Missing width/height causes layout shift -->
<img src="/image.jpg" alt="Image" loading="lazy" />
<!-- Bad: Width without height, browser can't calculate ratio -->
<img src="/image.jpg" alt="Image" width="800" loading="lazy" />
<!-- Bad: object-fill distorts image -->
<img src="/portrait.jpg" alt="Portrait" class="w-full h-64 object-fill" />
<!-- Good: explicit dimensions -->
<img
src="/image.jpg"
alt="Image"
width="800"
height="600"
loading="lazy"
class="w-full h-auto"
/>
<!-- Good: aspect-ratio with object-fit -->
<div class="aspect-video">
<img
src="/image.jpg"
alt="Image"
width="1600"
height="900"
loading="lazy"
class="w-full h-full object-cover"
/>
</div>Testing for Layout Shift
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Layout shift:', entry.value, entry.sources);
}
}).observe({ entryTypes: ['layout-shift'] });Lighthouse audit checks CLS score and verifies image elements have explicit width and height.
Lazy Loading and LCP Optimization
Native Lazy Loading
<!-- Lazy load (most images) -->
<img src="/image.jpg" alt="Image" loading="lazy" />
<!-- Eager load (default, LCP images) -->
<img src="/hero.jpg" alt="Hero" loading="eager" />When to Use Eager vs Lazy
Use loading="eager"
- LCP images:
loading="eager" fetchpriority="high" - Above-the-fold images (first 2-3 images visible on page load)
- Critical logos and branding
Use loading="lazy"
- Below-the-fold images
- Carousels and tabs (hidden until interaction)
- Long articles (images scattered throughout)
- Grid/masonry layouts (most below fold)
fetchpriority for LCP Optimization
<img
src="/hero-1200.jpg"
srcset="/hero-800.jpg 800w, /hero-1200.jpg 1200w, /hero-1600.jpg 1600w"
sizes="100vw"
alt="Hero image"
width="1600"
height="900"
loading="eager"
fetchpriority="high"
/>| Value | Meaning | Use Case |
|---|---|---|
high | Prioritize this resource | LCP images, critical assets |
low | Deprioritize this resource | Below-fold, non-critical |
auto | Browser decides (default) | Most images |
Browser Support
| Browser | Lazy Loading | fetchpriority |
|---|---|---|
| Chrome 77+ | Yes | Chrome 102+ |
| Firefox 121+ | Yes | Firefox 132+ |
| Safari 16.4+ | Yes | Safari 17.2+ |
| Edge 79+ | Yes | Edge 102+ |
Polyfill not needed -- gracefully degrades to eager loading.
Loading Distance Thresholds
| Browser | Distance | Notes |
|---|---|---|
| Chrome | 1250px on fast, 2500px on slow (4G) | Adapts to network |
| Firefox | 200px | Fixed threshold |
| Safari | ~100-200px | Fixed threshold |
Browsers handle loading distance intelligently based on connection speed.
Intersection Observer (Custom Lazy Loading)
For advanced use cases or blur-up placeholders:
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
});
document.querySelectorAll('img.lazy').forEach((img) => {
imageObserver.observe(img);
});<img data-src="/image.jpg" src="/placeholder.jpg" alt="Image" class="lazy" />When to use Intersection Observer:
- Legacy browser support: Safari < 15.4
- Custom loading distance: Different threshold than default
- Loading animations: Fade-in effects
- Placeholder strategies: Blur-up, LQIP
Blur-Up Placeholder Pattern
const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
const fullSrc = img.dataset.src;
const tempImg = new Image();
tempImg.onload = () => {
img.src = fullSrc;
img.classList.add('loaded');
};
tempImg.src = fullSrc;
observer.unobserve(img);
}
});
},
{ rootMargin: '50px' },
);.lazy {
filter: blur(10px);
transition: filter 0.3s;
}
.lazy.loaded {
filter: blur(0);
}Error Prevention
Don't Lazy Load LCP Images
<!-- Bad: delays LCP -->
<img src="/hero.jpg" alt="Hero" loading="lazy" />
<!-- Good: prioritizes LCP -->
<img src="/hero.jpg" alt="Hero" loading="eager" fetchpriority="high" />Don't Eager Load All Images
<!-- Bad: wastes bandwidth -->
<img src="/grid-item-20.jpg" alt="Item 20" loading="eager" />
<!-- Good: defer below-fold -->
<img src="/grid-item-20.jpg" alt="Item 20" loading="lazy" />Testing Lazy Loading
Chrome DevTools
1. Open DevTools, Network tab 2. Filter by "Img" 3. Throttle to "Slow 3G" 4. Reload page, scroll slowly 5. Observe images loading as they approach viewport
Lighthouse
- Offscreen Images: Checks if below-fold images use lazy loading
- LCP: Checks if LCP image is eagerly loaded
Manual Testing
document.querySelectorAll('img').forEach((img) => {
console.log(img.src, img.loading);
});
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.element);
}).observe({ entryTypes: ['largest-contentful-paint'] });Modern Image Formats
Format Comparison
| Format | Quality | File Size | Transparency | Browser Support |
|---|---|---|---|---|
| JPEG | Good | Medium | No | 100% |
| PNG | Lossless | Large | Yes | 100% |
| WebP | Excellent | Small | Yes | 96%+ |
| AVIF | Excellent | Smallest | Yes | 93%+ |
Real Example (1920x1080 photo): JPEG 500KB, WebP 250KB (-50%), AVIF 150KB (-70%).
Recommended Strategy: AVIF, WebP, JPEG fallback using <picture>.
AVIF with WebP and JPEG Fallback (Recommended)
<picture>
<source
srcset="/image-400.avif 400w, /image-800.avif 800w, /image-1200.avif 1200w"
sizes="(max-width: 768px) 100vw, 800px"
type="image/avif"
/>
<source
srcset="/image-400.webp 400w, /image-800.webp 800w, /image-1200.webp 1200w"
sizes="(max-width: 768px) 100vw, 800px"
type="image/webp"
/>
<img
src="/image-800.jpg"
srcset="/image-400.jpg 400w, /image-800.jpg 800w, /image-1200.jpg 1200w"
sizes="(max-width: 768px) 100vw, 800px"
alt="Image"
width="800"
height="600"
loading="lazy"
/>
</picture>Quality Recommendations
AVIF Quality Scale
| Use Case | Quality | File Size vs JPEG |
|---|---|---|
| Thumbnails | 50-65 | -70% |
| Most images | 65-75 | -60% |
| Hero images | 75-85 | -50% |
AVIF quality scale differs from JPEG/WebP. AVIF at 70 is roughly equivalent to JPEG at 90.
WebP Quality Scale
| Use Case | Quality | File Size vs JPEG |
|---|---|---|
| Thumbnails | 70-80 | -50% |
| Most images | 80-90 | -40% |
| Hero images | 90-95 | -30% |
File Size Targets
| Image Type | Target Size | Max Size |
|---|---|---|
| Hero image (1600w) | 150-250 KB | 500 KB |
| Content image (800w) | 80-120 KB | 200 KB |
| Card thumbnail (600w) | 40-80 KB | 150 KB |
| Thumbnail (300w) | 15-30 KB | 50 KB |
| Icon/logo (150w) | 5-15 KB | 30 KB |
Generating Modern Formats
Using Sharp (Node.js)
import sharp from 'sharp';
// Generate AVIF
await sharp('input.jpg').avif({ quality: 70 }).toFile('output.avif');
// Generate WebP
await sharp('input.jpg').webp({ quality: 80 }).toFile('output.webp');
// Generate responsive set
const widths = [400, 800, 1200, 1600];
await Promise.all(
widths.map((width) =>
sharp('input.jpg')
.resize(width)
.avif({ quality: 70 })
.toFile(`output-${width}.avif`),
),
);Using ImageMagick
# Convert to WebP
magick input.jpg -quality 80 output.webp
# Convert to AVIF
magick input.jpg -quality 70 output.avif
# Batch convert directory
for file in *.jpg; do
magick "$file" -quality 80 "${file%.jpg}.webp"
magick "$file" -quality 70 "${file%.jpg}.avif"
doneUsing cwebp (Official WebP Tool)
# Install cwebp
brew install webp # macOS
apt install webp # Ubuntu
# Convert to WebP
cwebp -q 80 input.jpg -o output.webp
# Lossless WebP
cwebp -lossless input.png -o output.webpUsing Cloudflare Images
const imageUrl = new URL(
'https://imagedelivery.net/account-hash/image-id/public',
);
imageUrl.searchParams.set('format', 'auto');
imageUrl.searchParams.set('quality', '80');
imageUrl.searchParams.set('width', '800');
// Cloudflare serves AVIF to Chrome 85+, WebP to Safari 14+, JPEG to olderFormat Selection Decision Tree
Need transparency or animation?
├── Yes -> AVIF/WebP with PNG fallback
└── No
├── Photo? -> AVIF -> WebP -> JPEG
└── Simple graphic? -> SVG (if possible)Common Mistakes
- Only serving JPEG -- Missing 50-70% potential size savings
- WebP without JPEG fallback -- Breaks in older browsers without WebP support
- Wrong source order -- JPEG before WebP means browser picks JPEG first
- Missing type attribute -- Browser downloads all sources to check format
Feature Detection (JavaScript)
async function supportsWebP() {
const webp =
'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=';
const blob = await fetch(webp).then((r) => r.blob());
return blob.type === 'image/webp';
}CSS Modern Format Backgrounds
Use image-set() for format negotiation in CSS backgrounds:
.hero {
background-image: url('/hero.jpg');
background-image: image-set(
url('/hero.avif') type('image/avif'),
url('/hero.webp') type('image/webp'),
url('/hero.jpg') type('image/jpeg')
);
}Picture Element and Art Direction
What is Art Direction?
Art direction means serving different crops or compositions of an image based on viewport size, not just scaling the same image.
| Use Case | Mobile | Desktop | Why |
|---|---|---|---|
| Portrait Product | Vertical crop | Horizontal crop | Show product differently |
| Hero with Text | Tight crop, text outside | Wide shot, text overlaid | Layout changes |
| Group Photo | Face close-up | Full group | Show detail vs context |
Basic Syntax
<picture>
<source media="(max-width: 640px)" srcset="/image-portrait.jpg" />
<source media="(min-width: 641px)" srcset="/image-landscape.jpg" />
<img src="/image-landscape.jpg" alt="Image" />
</picture>Art Direction with Responsive Sizes
Combine media queries with srcset for both art direction AND responsive sizing:
<picture>
<source
media="(max-width: 640px)"
srcset="/product-portrait-400.jpg 400w, /product-portrait-800.jpg 800w"
sizes="100vw"
/>
<source
media="(min-width: 641px) and (max-width: 1024px)"
srcset="/product-square-600.jpg 600w, /product-square-1200.jpg 1200w"
sizes="90vw"
/>
<source
media="(min-width: 1025px)"
srcset="
/product-landscape-800.jpg 800w,
/product-landscape-1200.jpg 1200w,
/product-landscape-1600.jpg 1600w
"
sizes="1200px"
/>
<img
src="/product-landscape-1200.jpg"
alt="Product image"
width="1200"
height="675"
loading="lazy"
/>
</picture>Art Direction + Modern Formats
Combine art direction with format selection using nested sources:
<picture>
<!-- Mobile Portrait: AVIF -->
<source
media="(max-width: 640px)"
srcset="/hero-portrait-400.avif 400w, /hero-portrait-800.avif 800w"
sizes="100vw"
type="image/avif"
/>
<!-- Mobile Portrait: WebP -->
<source
media="(max-width: 640px)"
srcset="/hero-portrait-400.webp 400w, /hero-portrait-800.webp 800w"
sizes="100vw"
type="image/webp"
/>
<!-- Mobile Portrait: JPEG -->
<source
media="(max-width: 640px)"
srcset="/hero-portrait-400.jpg 400w, /hero-portrait-800.jpg 800w"
sizes="100vw"
/>
<!-- Desktop Landscape: AVIF -->
<source
media="(min-width: 641px)"
srcset="
/hero-landscape-800.avif 800w,
/hero-landscape-1200.avif 1200w,
/hero-landscape-1600.avif 1600w
"
sizes="100vw"
type="image/avif"
/>
<!-- Desktop Landscape: WebP -->
<source
media="(min-width: 641px)"
srcset="
/hero-landscape-800.webp 800w,
/hero-landscape-1200.webp 1200w,
/hero-landscape-1600.webp 1600w
"
sizes="100vw"
type="image/webp"
/>
<!-- Desktop Landscape: JPEG (fallback) -->
<img
src="/hero-landscape-1200.jpg"
srcset="
/hero-landscape-800.jpg 800w,
/hero-landscape-1200.jpg 1200w,
/hero-landscape-1600.jpg 1600w
"
sizes="100vw"
alt="Hero image"
width="1600"
height="900"
loading="eager"
fetchpriority="high"
/>
</picture>Browser Selection Logic: media query first, then type (AVIF, WebP, JPEG), then size from srcset + sizes.
Orientation-Based Art Direction
<picture>
<source
media="(orientation: portrait)"
srcset="/image-portrait-600.jpg 600w, /image-portrait-1200.jpg 1200w"
sizes="100vw"
/>
<img
src="/image-landscape-1200.jpg"
srcset="
/image-landscape-800.jpg 800w,
/image-landscape-1200.jpg 1200w,
/image-landscape-1600.jpg 1600w
"
sizes="100vw"
alt="Image"
width="1600"
height="900"
/>
</picture>When NOT to Use Art Direction
- Same crop works at all sizes (just use
srcset) - Only format conversion needed (use
typeonly) - Minor composition adjustments (CSS
object-positionmay suffice)
Common Mistakes
Missing Fallback img
<!-- Bad: doesn't render without picture support -->
<picture>
<source media="(max-width: 640px)" srcset="/image-mobile.jpg" />
</picture>
<!-- Good: always include img fallback -->
<picture>
<source media="(max-width: 640px)" srcset="/image-mobile.jpg" />
<img src="/image-desktop.jpg" alt="Image" width="1200" height="675" />
</picture>Wrong Source Order
Correct order: media THEN type THEN size selection. Place most specific format (AVIF) before less specific (WebP) before fallback (JPEG).
Missing sizes in srcset Sources
<!-- Bad: Browser defaults to 100vw for all sizes -->
<source
media="(max-width: 640px)"
srcset="/image-400.jpg 400w, /image-800.jpg 800w"
/>
<!-- Good: includes sizes -->
<source
media="(max-width: 640px)"
srcset="/image-400.jpg 400w, /image-800.jpg 800w"
sizes="100vw"
/>srcset and sizes Attributes
Width Descriptors (w) - Recommended
Width descriptors tell the browser the actual width of each image file in pixels. The browser uses this with the sizes attribute and device pixel ratio to choose the optimal image.
<img
src="/image-800.jpg"
srcset="
/image-400.jpg 400w,
/image-800.jpg 800w,
/image-1200.jpg 1200w,
/image-1600.jpg 1600w
"
sizes="(max-width: 768px) 100vw, 800px"
alt="Responsive image"
width="800"
height="600"
/>Browser Selection Logic:
1. Calculate display width from sizes attribute 2. Multiply by device pixel ratio (DPR) 3. Choose smallest image >= calculated width 4. Consider network conditions and cache
Example: On a 375px wide phone (2x DPR):
sizesevaluates to375px(100vw on mobile)- Multiply by DPR:
375 * 2 = 750px - Browser chooses
image-800.jpg(smallest >= 750px)
Density Descriptors (x) - For Fixed Sizes Only
Only use for fixed-size images like logos:
<img
src="/logo.png"
srcset="/logo.png 1x, /logo@2x.png 2x, /logo@3x.png 3x"
alt="Company logo"
width="150"
height="50"
/>Common sizes Patterns
<!-- Full width -->
sizes="100vw"
<!-- Content width (max 800px) -->
sizes="(max-width: 768px) 100vw, 800px"
<!-- Sidebar (fixed 300px) -->
sizes="300px"
<!-- 2-column grid -->
sizes="(max-width: 768px) 100vw, 50vw"
<!-- 3-column grid -->
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
<!-- Responsive with max-width -->
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 90vw, 1200px"
<!-- Grid with gaps (12 cols, 3-wide, 2rem gap) -->
sizes="(max-width: 768px) 100vw, calc((100vw - 4rem) / 3)"Recommended Breakpoints
| Use Case | Recommended Widths | Reasoning |
|---|---|---|
| Full-width hero | 800w, 1200w, 1600w, 2400w | Covers mobile, tablet, desktop, retina |
| Content images | 400w, 800w, 1200w | Covers mobile, tablet, desktop at 1x/2x DPR |
| Grid cards (3-col) | 300w, 600w, 900w | Covers ~33vw at 1x/2x/3x DPR |
| Thumbnails | 150w, 300w | Small fixed-size images at 1x/2x |
DPR Considerations
| Device Type | Typical DPR | Example |
|---|---|---|
| Standard desktop | 1x | Older monitors |
| Retina desktop | 2x | MacBook Pro, 4K monitors |
| Standard mobile | 2x | iPhone 11, Pixel 4 |
| High-end mobile | 3x | iPhone 14 Pro, Samsung S23 |
Formula: Display Width x Max DPR = Image Width
Common Mistakes
Missing sizes Attribute
<!-- Bad: Browser defaults to 100vw, wastes bandwidth -->
<img
src="/image-800.jpg"
srcset="/image-400.jpg 400w, /image-800.jpg 800w"
alt="Image"
/>
<!-- Good: specifies display size -->
<img
src="/image-800.jpg"
srcset="/image-400.jpg 400w, /image-800.jpg 800w"
sizes="(max-width: 768px) 100vw, 800px"
alt="Image"
width="800"
height="600"
/>Using Density Descriptors for Responsive Images
<!-- Bad: only considers DPR, not viewport -->
<img src="/image.jpg" srcset="/image.jpg 1x, /image@2x.jpg 2x" alt="Image" />
<!-- Good: considers viewport + DPR -->
<img
src="/image-800.jpg"
srcset="/image-400.jpg 400w, /image-800.jpg 800w, /image-1200.jpg 1200w"
sizes="(max-width: 768px) 100vw, 800px"
alt="Image"
width="800"
height="600"
/>Tips for Writing sizes
1. Mobile-first: Start with mobile sizes, then add larger breakpoints 2. Match layout breakpoints: Use same breakpoints as your CSS 3. Consider container width: Account for padding, gaps, max-width 4. Test with DevTools: Chrome DevTools shows which image was selected 5. Don't overthink: Browser is smart, close approximations work fine
Testing Tools
- Chrome DevTools: Right-click image, Inspect, check
currentSrcin Elements panel - Firefox DevTools: Computed tab shows
currentSrc, Responsive Design Mode for viewports - Online: Responsive Image Breakpoints Generator, RespImageLint