Skip to main content

    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

    Use srcset + sizes so the browser downloads the right-sized image
    Tell the w (width) and x (density) descriptors apart and pick the right one
    Serve art-directed crops and WebP/AVIF formats with <picture>
    Defer off-screen images with loading="lazy" and decoding="async"
    Reserve space with width/height + aspect-ratio to stop layout shift (CLS)
    Fit images to any container with object-fit and object-position
    Before this lesson: you should be comfortable writing the <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

    Try it Yourself ยป
    Code Preview
    <!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

    Try it Yourself ยป
    Code Preview
    <!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

    Try it Yourself ยป
    Code Preview
    <!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

    Try it Yourself ยป
    Code Preview
    <!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

    Try it Yourself ยป
    Code Preview
    <!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

    Try it Yourself ยป
    Code Preview
    <!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

    Try it Yourself ยป
    Code Preview
    <!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

    Try it Yourself ยป
    Code Preview
    <!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/height off your <img>, so the browser reserves no space until the file arrives. Fix: add width and height attributes (keep height: auto in CSS for fluidity), or set aspect-ratio on the container.
    • srcset vs sizes confusion โ€” wrong file gets picked. Using w descriptors in srcset without a sizes attribute makes the browser assume 100vw and often over-download. Fix: always pair w descriptors with a sizes value 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 in srcset so 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 the alt.
    • 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-fit only 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 applying cover/contain.

    ๐Ÿ“‹ Quick Reference

    GoalUse this
    Serve sizes of the same imagesrcset="a.jpg 400w, b.jpg 800w"
    Describe the display slotsizes="(max-width:600px) 100vw, 60vw"
    Fixed-size image on retinasrcset="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 downloadloading="lazy" decoding="async"
    Reserve space (no CLS)width="800" height="450" + aspect-ratio: 16/9
    Fill a box without distortionobject-fit: cover;
    Steer the cropobject-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-ratio reserve space to kill layout shift (CLS)
    • โœ… object-fit: cover + object-position fit 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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy Policy โ€ข Terms of Service