🖼️ 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.
- 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"
>
| Attribute | Why 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) |
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 */
}
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"
>
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.
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
100vwwide (full width) - On screens 601px–1024px → image is
50vwwide (half the viewport) - Otherwise (desktop) → image is a fixed
800px
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>
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>
| Format | Compression vs JPEG | Browser support | Best 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 -->
>
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.
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;
}
(distorts)
object-fit: fill
whole image
object-fit: contain
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
<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
- Always set
alt,width, andheighton every<img> max-width: 100%; height: auto;makes images fluid in CSSsrcsetwithwdescriptors offers multiple resolutions — browser picks the bestsizestells 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: covercrops images to fill a fixed container without distortionaspect-ratioreserves the correct space before the image downloads, preventing layout shift