🖼️ Responsive Images

Images are typically the heaviest assets on a web page. A 2000px photo loaded on a 400px mobile screen wastes bandwidth, slows the page, and drains the user's battery. Responsive images fix this by serving the right image at the right size for every device.

📝 What "responsive images" covers
  • Resolution switching — serving a smaller file on small screens
  • Art direction — serving a different crop or composition on small screens
  • Format switching — serving WebP/AVIF to browsers that support them
  • CSS sizing — making images scale properly inside layouts

The Basics: Always Set These

Even before touching srcset or <picture>, every image should have these three things.

<!-- ✅ Always include: alt, width, height -->
<img
  src="photo.jpg"
  alt="A brown dog sitting on a green lawn"
  width="800"
  height="600"
>
AttributeWhy it matters
alt Screen readers read it aloud; shown when image fails to load; required for accessibility
width + height Browser reserves space before the image loads, preventing layout shift (CLS)
💡 width/height don't lock the size

Setting width="800" height="600" in HTML just tells the browser the aspect ratio so it can reserve the right space. You still control the actual display size with CSS. Add max-width: 100%; height: auto; and the image will scale freely while keeping its proportions.

img {
  max-width: 100%;
  height: auto; /* maintains aspect ratio */
}
⚠️ Empty alt for decorative images

If an image is purely decorative (a background flourish, a divider icon), use alt="". Screen readers will skip it entirely. Never omit alt — missing it causes screen readers to read the filename.

<img src="divider.svg" alt=""> <!-- decorative: empty alt -->
<img src="team.jpg"    alt="The engineering team at our 2024 offsite">

srcset — Resolution Switching

The srcset attribute lets you offer multiple versions of the same image at different sizes. The browser picks the best one based on the screen size and device pixel ratio.

Width descriptors (w)

<img
  src="photo-800.jpg"
  srcset="
    photo-400.jpg  400w,
    photo-800.jpg  800w,
    photo-1200.jpg 1200w
  "
  alt="Mountain landscape"
  width="800"
  height="533"
>
📝 How the browser chooses

The w descriptor tells the browser the intrinsic width of each image file in pixels. The browser combines this with the display size and the device pixel ratio to pick the most efficient file.

On a 400px-wide mobile with a 2× retina screen, it needs a 800px image — so it picks photo-800.jpg, not the 1200px version.

⚠️ src is the fallback

Always include a src attribute. Browsers that don't support srcset fall back to it. Use a mid-size image as the fallback.

Pixel density descriptors (x)

When the image always displays at a fixed size but you want to serve a sharper version for retina screens, use x descriptors instead.

<!-- Logo: always 200px wide, but crisp on retina -->
<img
  src="logo.png"
  srcset="logo.png 1x, logo@2x.png 2x, logo@3x.png 3x"
  alt="Company logo"
  width="200"
  height="60"
>

sizes — Telling the Browser the Display Width

By default the browser doesn't know how wide your image will be displayed (that's determined by CSS, which hasn't been parsed yet). The sizes attribute tells it before the CSS loads, so it can start downloading the right file immediately.

<img
  src="photo-800.jpg"
  srcset="
    photo-400.jpg  400w,
    photo-800.jpg  800w,
    photo-1200.jpg 1200w,
    photo-1600.jpg 1600w
  "
  sizes="
    (max-width: 600px)  100vw,
    (max-width: 1024px) 50vw,
    800px
  "
  alt="Mountain landscape"
  width="800"
  height="533"
>

Reading the sizes value above:

  • On screens 600px or narrower → image is 100vw wide (full width)
  • On screens 601px–1024px → image is 50vw wide (half the viewport)
  • Otherwise (desktop) → image is a fixed 800px
💡 srcset + sizes together

srcset lists the available files and their widths. sizes tells the browser how wide the image will actually appear. Together, the browser can calculate which file wastes the least bandwidth. Always use them together when using w descriptors.

<picture> — Art Direction and Format Switching

The <picture> element gives you full control over which image source is used. It wraps multiple <source> elements and one fallback <img>.

Art direction — different crops per screen size

Sometimes a landscape photo that looks great on desktop needs a tighter, portrait crop to work on mobile. srcset alone can't do this — it only switches resolution, not composition.

<picture>
  <!-- Mobile: tall portrait crop -->
  <source
    media="(max-width: 600px)"
    srcset="hero-portrait.jpg"
  >
  <!-- Tablet: square crop -->
  <source
    media="(max-width: 1024px)"
    srcset="hero-square.jpg"
  >
  <!-- Desktop: full landscape -->
  <img
    src="hero-landscape.jpg"
    alt="A wide mountain panorama"
    width="1200"
    height="600"
  >
</picture>
📝 How <picture> works

The browser checks each <source> in order and uses the first one that matches. The <img> at the end is both the final fallback and the element that carries alt, width, and height.

Format switching — WebP and AVIF with fallback

Modern image formats like WebP and AVIF are 30–80% smaller than JPEG at the same quality. Not all browsers support them, so you serve the modern format to those that can and fall back to JPEG for the rest.

<picture>
  <!-- AVIF: best compression, newest (Chrome, Firefox, Safari 16+) -->
  <source
    type="image/avif"
    srcset="photo-400.avif 400w, photo-800.avif 800w, photo-1200.avif 1200w"
  >
  <!-- WebP: great compression, very wide support -->
  <source
    type="image/webp"
    srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1200.webp 1200w"
  >
  <!-- JPEG: universal fallback -->
  <img
    src="photo-800.jpg"
    srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w"
    sizes="(max-width: 600px) 100vw, 800px"
    alt="Mountain landscape"
    width="800"
    height="533"
  >
</picture>
FormatCompression vs JPEGBrowser supportBest for
JPEG Baseline Universal Photos, fallback
PNG Larger (lossless) Universal Logos, screenshots, transparency
WebP ~30% smaller All modern browsers Photos and graphics
AVIF ~50% smaller Chrome, Firefox, Safari 16+ Photos where size matters most
SVG Tiny for vectors Universal Icons, logos, illustrations

Lazy Loading

By default the browser loads all images on the page, even those far below the fold that the user may never scroll to. Lazy loading defers off-screen images until they are about to enter the viewport.

<!-- Defer loading until the image is near the viewport -->
<img
  src="photo.jpg"
  alt="..."
  width="800"
  height="600"
  loading="lazy"
>

<!-- Above-the-fold images should load eagerly (default) -->
<img
  src="hero.jpg"
  alt="..."
  width="1200"
  height="600"
  loading="eager"  <!-- default, can be omitted -->
>
💡 loading="lazy" is a one-liner win

Adding loading="lazy" to every below-the-fold image is one of the easiest performance improvements you can make. It is natively supported in all modern browsers and requires zero JavaScript.

⚠️ Don't lazy-load hero images

The first visible image (hero, logo, above-the-fold photo) should never be lazy-loaded. Delaying it hurts LCP (Largest Contentful Paint), one of Google's Core Web Vitals that affects SEO rankings.

decoding and fetchpriority

Two more attributes that give you fine-grained control over image loading:

<!-- decoding="async": decode the image off the main thread
     so it doesn't block rendering of other content -->
<img
  src="photo.jpg"
  alt="..."
  decoding="async"
  loading="lazy"
>

<!-- fetchpriority="high": tell the browser to download this
     image ASAP — use on the hero/LCP image -->
<img
  src="hero.jpg"
  alt="..."
  fetchpriority="high"
>

CSS: Controlling How Images Fit

object-fit and object-position

When you give an image a fixed width and height in CSS, the image may be a different shape than the container. object-fit controls how the image fills that space.

/* Stretch to fill — may distort the image (default) */
img { object-fit: fill; }

/* Scale down to fit entirely inside — may leave empty space */
img { object-fit: contain; }

/* Crop to fill the box — no distortion, most common */
img { object-fit: cover; }

/* Control where the crop focuses */
img {
  object-fit: cover;
  object-position: center top; /* keep the top in frame (faces) */
}

/* Practical: uniform card thumbnails regardless of image shape */
.card-thumbnail {
  width: 100%;
  height: 200px;
  object-fit: cover;
  object-position: center;
  border-radius: 8px 8px 0 0;
}
fill
(distorts)

object-fit: fill

contains
whole image

object-fit: contain

cropped
to fill

object-fit: cover

aspect-ratio — reserving space before the image loads

/* Reserve 16:9 space while image downloads */
.hero-img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* Square avatar */
.avatar {
  width: 64px;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 50%;
}

/* Card thumbnail — 4:3 */
.thumbnail {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

Practical Patterns

The complete responsive image

Production-ready image combining all techniques
<picture>
  <source
    type="image/avif"
    srcset="img-400.avif 400w, img-800.avif 800w, img-1200.avif 1200w"
    sizes="(max-width: 600px) 100vw, (max-width: 1024px) 50vw, 600px"
  >
  <source
    type="image/webp"
    srcset="img-400.webp 400w, img-800.webp 800w, img-1200.webp 1200w"
    sizes="(max-width: 600px) 100vw, (max-width: 1024px) 50vw, 600px"
  >
  <img
    src="img-800.jpg"
    srcset="img-400.jpg 400w, img-800.jpg 800w, img-1200.jpg 1200w"
    sizes="(max-width: 600px) 100vw, (max-width: 1024px) 50vw, 600px"
    alt="Describe the image content here"
    width="800"
    height="533"
    loading="lazy"
    decoding="async"
  >
</picture>

Hero image (above the fold)

<!-- Hero: eager, high priority, no lazy -->
<picture>
  <source type="image/webp"
    srcset="hero-800.webp 800w, hero-1600.webp 1600w"
    sizes="100vw"
  >
  <img
    src="hero-1600.jpg"
    alt="..."
    width="1600"
    height="900"
    fetchpriority="high"
    loading="eager"
  >
</picture>

Photo grid / gallery

/* CSS: consistent grid with cover cropping */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1rem;
}

.gallery img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 8px;
}
<!-- HTML: lazy load everything in the grid -->
<div class="gallery">
  <img src="photo1.jpg" alt="..." width="800" height="600" loading="lazy">
  <img src="photo2.jpg" alt="..." width="800" height="600" loading="lazy">
  <img src="photo3.jpg" alt="..." width="800" height="600" loading="lazy">
</div>

Circular avatar

<img
  src="avatar.jpg"
  srcset="avatar.jpg 1x, avatar@2x.jpg 2x"
  alt="Profile photo of Jane Doe"
  width="64"
  height="64"
  class="avatar"
>

/* CSS */
.avatar {
  border-radius: 50%;
  object-fit: cover;
  object-position: center top; /* keep face in frame */
}

Summary

📚 What you learned
  • Always set alt, width, and height on every <img>
  • max-width: 100%; height: auto; makes images fluid in CSS
  • srcset with w descriptors offers multiple resolutions — browser picks the best
  • sizes tells the browser how wide the image will display before CSS loads
  • <picture> enables art direction (different crops) and format switching (WebP/AVIF)
  • Serve AVIF → WebP → JPEG in order of preference for best compression
  • loading="lazy" defers off-screen images — use on everything below the fold
  • Never lazy-load the hero image — use fetchpriority="high" instead
  • object-fit: cover crops images to fill a fixed container without distortion
  • aspect-ratio reserves the correct space before the image downloads, preventing layout shift