Lesson 27 โข Advanced Track
Responsive Images: srcset, sizes & picture
By the end of this lesson you'll serve every visitor an image sized for their screen, swap crops and modern formats with <picture>, lazy-load what's off-screen, and reserve space so your page never jumps while images load.
What You'll Learn
<img> tag with src and alt, and know the basics of CSS properties and media queries. If @media queries feel new, review Responsive Design first.๐ก Think of It Like This
Responsive images are like a coffee shop offering small, medium and large cups. You wouldn't pour a venti into someone who asked for an espresso โ you'd waste coffee and they couldn't drink it all. A 4000-pixel photo sent to a phone is exactly that waste: megabytes the screen can't even show, paid for by the visitor's data plan and your load time.
With srcset you hand the browser the whole menu of sizes and let it order the cup that fits the cup-holder (sizes). The phone sips a 400px file; the 4K monitor gets the 2000px one. Everyone gets a full cup, nobody pays for spillage โ and because you reserved the right-sized cup-holder up front (aspect-ratio), the table never wobbles while the drink arrives.
1. srcset & sizes โ Resolution Switching
Images are usually the heaviest thing on a page โ often 60โ80% of its total weight. Resolution switching means shipping the same picture at several file sizes and letting the browser download whichever fits the screen. You list the files in srcset, each tagged with its real pixel width using the w descriptor (800w means "this file is 800 pixels wide").
On its own, srcset isn't enough โ the browser chooses the file before it has laid out the page, so it can't yet know how wide the image will appear. That's the job of sizes: it tells the browser, for each viewport width, how wide the image's slot will be. The browser combines sizes with the device's pixel density to pick the smallest file that still looks sharp.
Run the worked example, then resize the preview. Read every comment โ they state exactly what each attribute does.
Worked example: srcset + sizes
The browser picks a file from the menu based on the slot width
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>srcset + sizes</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }
img { width: 100%; height: auto; border-radius: 8px; display: block; }
.note { background:#e0f2fe; color:#075985; padding:12px 16px; border-radius:8px; margin:16px 0; }
code { background:#bae6fd; padding:2px 6px; border-radius:4px; }
</style>
</head>
<body>
<h1>Resolution switc
...2. The <picture> Element โ Art Direction & Modern Formats
Where srcset serves the same image at different sizes, <picture> lets you serve different images entirely. This is called art direction: a wide landscape crop on desktop, but a tight square or portrait crop on mobile so the subject stays large enough to see. You wrap one or more <source> elements and a final fallback <img> inside it.
The browser reads the <source> elements top to bottom and uses the first one that matches. Each source can match on a media query (for art direction) or a type (for formats like image/avif and image/webp). The trailing <img> is mandatory โ it's both the fallback for old browsers and the element that actually carries the alt text.
Worked example: <picture> for crop + format
First matching <source> wins; the <img> is the fallback
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>picture element</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; max-width: 700px; margin: 0 auto; }
img { width: 100%; height: auto; border-radius: 8px; display: block; }
.note { background:#e0f2fe; color:#075985; padding:12px 16px; border-radius:8px; margin:16px 0; }
pre { background:#1e293b; color:#e2e8f0; padding:16px; border-radius:8px; overflow-x:auto; font-size:.85rem; }
...3. loading="lazy" & decoding="async" โ Faster Pages
loading="lazy" tells the browser to defer downloading an image until the user scrolls near it. On a long gallery that can turn dozens of downloads into just the two or three that are actually on screen, slashing your initial page weight. The catch: never lazy-load an image that's visible on first paint (your hero) โ deferring it delays your Largest Contentful Paint and hurts Core Web Vitals.
decoding="async" is a smaller win that pairs nicely: it lets the browser decode the image off the main thread so a big picture can't briefly freeze scrolling or interaction. Use it on content images you're not racing to paint immediately.
Worked example: a lazy-loaded gallery
Off-screen images wait until you scroll to them
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>lazy loading</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; max-width: 640px; margin: 0 auto; }
.gallery { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.gallery img { width: 100%; height: 180px; object-fit: cover; border-radius: 8px; }
.tip { background:#fff7ed; color:#9a3412; border-left:4px solid #f97316; padding:12px 16px; border-radius:8px; margin:16px
...4. width/height + aspect-ratio โ Stop the Layout Shift
Ever seen text leap down the page as an image pops in? That's Cumulative Layout Shift (CLS), a Core Web Vital you can score badly on. It happens because the browser doesn't know how tall an image will be until the file arrives, so it reserves no space and then shoves everything down once it loads.
Two fixes, used together. First, always put width and height attributes on every <img> โ even when CSS makes it fluid with width: 100%; height: auto;. The browser reads them as a ratio and reserves the right box up front. Second, for CSS-driven boxes (or placeholders), set the aspect-ratio property so the container holds its shape before anything loads.
Worked example: reserved space, zero CLS
aspect-ratio holds the box so the page never jumps
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>aspect-ratio</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; max-width: 700px; margin: 0 auto; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px,1fr)); gap: 16px; }
.card { border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
/* width:100% + height:auto keeps the image fluid... */
.card img { width: 100%; height: auto; display: bloc
...5. object-fit & object-position โ Fit Any Box
When you force an image into a fixed box, its own shape rarely matches. By default it stretches and looks squashed. object-fit fixes that โ it's like background-size, but for a real <img>. cover fills the box and crops the overflow (no distortion); contain fits the whole image inside and may leave gaps; fill stretches it (the ugly default).
When cover crops, it crops from the centre by default โ which can chop off a head. object-position lets you steer which part survives the crop, e.g. object-position: top; to keep faces in frame.
Worked example: object-fit values side by side
cover vs contain vs fill, plus object-position
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>object-fit</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; max-width: 760px; margin: 0 auto; }
.row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px,1fr)); gap: 16px; }
figure { margin: 0; }
figcaption { font-family: monospace; font-size: .8rem; color:#475569; margin-top:6px; text-align:center; }
/* Every box is the SAME fixed size so we can compare how
...๐ฏ Your Turn #1 โ Wire up srcset + sizes
The image below only has a single src, so every device downloads the big file. Give the browser a menu of sizes to choose from. Fill in the blanks marked ___, then run it and check the expected result in the comments.
Your Turn #1: add srcset + sizes
Offer three widths and describe the slot
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Turn 1</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }
img { width: 100%; height: auto; border-radius: 8px; display: block; }
</style>
</head>
<body>
<h1>Make this image responsive</h1>
<!-- ๐ฏ YOUR TURN โ fill in the blanks marked ___ -->
<img
src="https://picsum.photos/1200/600"
/* 1) Offer THREE files with their real widths (
...๐ฏ Your Turn #2 โ Art direction + a no-CLS box
Build a <picture> that swaps crop at 600px, give the fallback <img> its dimensions so the page doesn't jump, and crop it to a fixed shape without distortion. Fill in each ___ and verify against the comments.
Your Turn #2: picture + width/height + object-fit
Swap the crop, reserve the box, fill it cleanly
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Turn 2</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; max-width: 700px; margin: 0 auto; }
.hero { aspect-ratio: 16 / 9; border-radius: 12px; overflow: hidden; }
/* 3) Make the image FILL the .hero box and crop overflow, no stretching. */
.hero img { width: 100%; height: 100%; object-fit: ___; display: block; }
/* ๐ replace ___ with cover */
</style>
</head>
<body>
...๐งฉ Mini-Challenge โ A performant card from scratch
Support is faded now โ only an outline is given. Build one image card that uses everything from this lesson. Lean on the worked examples in sections 1โ5 if you get stuck.
Mini-Challenge: responsive image card
srcset + sizes, dimensions, aspect-ratio, object-fit, lazy
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mini-Challenge</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; max-width: 480px; margin: 0 auto; }
/* ๐งฉ MINI-CHALLENGE: a fast, shift-free image card
1. Style a .card with a border and rounded corners (overflow: hidden)
2. Give .card .thumb an aspect-ratio (e.g. 16 / 9) so its box is
reserved BEFORE the image loads (this kills CLS)
3. Make .card .thumb im
...โ ๏ธ Common Errors (and the fix)
- The page jumps as images load (CLS). You left
width/heightoff your<img>, so the browser reserves no space until the file arrives. Fix: addwidthandheightattributes (keepheight: autoin CSS for fluidity), or setaspect-ratioon the container. - srcset vs sizes confusion โ wrong file gets picked. Using
wdescriptors insrcsetwithout asizesattribute makes the browser assume100vwand often over-download. Fix: always pairwdescriptors with asizesvalue that describes the real slot width. - Shipping one huge image to everyone. A single
src="photo-2000.jpg"wastes megabytes on phones and tanks your load time. Fix: export the photo at a few widths and list them insrcsetso each device downloads only what it needs. - Missing the fallback <img> inside <picture>. A
<picture>with only<source>elements shows nothing where none match. Fix: end every<picture>with a plain<img>โ it carries the fallback and thealt. - Lazy-loading the hero.
loading="lazy"on an above-the-fold image delays Largest Contentful Paint and hurts Core Web Vitals. Fix: only lazy-load below-the-fold images; let the hero load eagerly. - object-fit does nothing.
object-fitonly has an effect when the image has a fixed width and height to fill. Fix: give the<img>explicit dimensions (or a sized container) before applyingcover/contain.
๐ Quick Reference
| Goal | Use this |
|---|---|
| Serve sizes of the same image | srcset="a.jpg 400w, b.jpg 800w" |
| Describe the display slot | sizes="(max-width:600px) 100vw, 60vw" |
| Fixed-size image on retina | srcset="i.png 1x, i@2x.png 2x" |
| Different crop per viewport | <source media="(min-width:600px)" srcset="โฆ"> |
| Modern format + fallback | <source type="image/avif" srcset="โฆ"> |
| Defer off-screen download | loading="lazy" decoding="async" |
| Reserve space (no CLS) | width="800" height="450" + aspect-ratio: 16/9 |
| Fill a box without distortion | object-fit: cover; |
| Steer the crop | object-position: top; |
โ Frequently Asked Questions
When should I use srcset versus the <picture> element?
Use srcset (with sizes) when you are serving the same image at different sizes and just want the browser to pick the most efficient file โ this is the common case for almost every content image. Use <picture> when you need to change the image itself depending on the situation: a different crop on mobile than desktop (art direction), or a modern format like WebP/AVIF with a JPEG fallback. A quick rule of thumb: if the only thing changing is the resolution, reach for srcset; if the actual pixels (crop, format) change, reach for <picture>.
What is the difference between the w and x descriptors in srcset?
The w descriptor tells the browser the real pixel width of each file โ image-800.jpg 800w means that file is 800 pixels wide. You pair w descriptors with the sizes attribute so the browser can work out which file fits the slot. The x descriptor describes pixel density instead โ logo@2x.png 2x means 'use this on a 2x retina screen'. Use w for fluid content images that resize with the layout, and x for fixed-size images like icons and logos that are always displayed at one CSS size.
Why does my page jump around while images load?
That jump is Cumulative Layout Shift (CLS), and it happens when the browser does not know how tall an image will be until the file downloads, so it reserves zero height and then shoves everything down once the image arrives. The fix is to tell the browser the dimensions up front: put width and height attributes on every <img>, or set an aspect-ratio in CSS. The browser then reserves the correct box immediately and the image fills it in place โ no jump.
Do I still need width and height attributes if my image is fluid with width: 100%?
Yes โ and this surprises people. Even when CSS overrides the displayed width (img { width: 100%; height: auto; }), the width and height attributes still give the browser the intrinsic aspect ratio, which it uses to reserve the right amount of vertical space before the file loads. Modern browsers combine the attributes with your height: auto to compute the correct box. Leave them off and you lose your CLS protection, so always include them.
Is loading="lazy" safe to put on every image?
Almost โ but never put it on your hero or any image that is visible when the page first loads (above the fold). Lazy loading defers the download until the image is near the viewport, which is perfect for off-screen images but delays your Largest Contentful Paint (LCP) if applied to the first thing the user sees. Rule: loading="lazy" for below-the-fold images, and let above-the-fold images load eagerly (the default), optionally with fetchpriority="high" on the hero.
Are WebP and AVIF safe to use in production?
Yes, with a fallback. WebP is supported in every current browser, and AVIF is supported in all the major ones too, but you should never serve them alone in case a visitor is on something older. The <picture> element handles this for you: list the modern formats as <source type="image/avif"> and <source type="image/webp"> first, then a plain <img src="photo.jpg"> last. The browser picks the first format it understands and falls back to the JPEG if it understands none.
๐ Lesson Complete
You can now ship images that are fast, sharp on every screen, and don't shift the layout. The essentials:
- โ
srcset+sizes= resolution switching (same image, right-sized file) - โ
<picture>= art direction and AVIF/WebP with a JPEG fallback (first matching <source> wins) - โ
loading="lazy"+decoding="async"defer off-screen work โ never on the hero - โ
width/height+aspect-ratioreserve space to kill layout shift (CLS) - โ
object-fit: cover+object-positionfit any box without distortion
Next up: Transitions, where you'll animate state changes smoothly with CSS.
Sign up for free to track which lessons you've completed and get learning reminders.