Responsive Images & Typography
Serve the right image at the right size and make text scale fluidly across every screen.
1. Introduction
Images are often the largest assets on a web page. Serving a 2000 px wide image to a phone with a 400 px screen wastes bandwidth, slows load time, and drains the user's data plan. Similarly, fixed font sizes that look comfortable on a desktop can be uncomfortably large or small on other screen sizes.
In this lesson you will learn two complementary skills:
- Responsive images — letting the browser pick the best image file for the current screen width and pixel density.
- Responsive (fluid) typography — making font sizes scale smoothly between a minimum and maximum value using
clamp().
By the end you will be able to write srcset, sizes, and <picture> elements, choose modern image formats, and write a one-liner fluid type scale.
2. Theory
2.1 Why plain <img src> is not enough
A single src attribute sends the same file to every device:
- A 1 400 px image on a 375 px phone — 4× the pixels needed.
- A 400 px image on a Retina display — blurry because 1 CSS pixel = 2 device pixels.
Responsive images give the browser information so it can choose the best file itself.
2.2 The srcset attribute (resolution switching)
srcset lists candidate image files and their intrinsic widths (using the w descriptor).
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1600.jpg 1600w"
alt="Mountain landscape"
>
The browser combines the candidate widths with the actual rendered width of the image (from sizes) and the device pixel ratio to select a file.
2.3 The sizes attribute
sizes tells the browser how wide the image will be rendered at each breakpoint — before it downloads the CSS. Without sizes, the browser assumes 100vw.
<img
src="card-800.jpg"
srcset="card-400.jpg 400w, card-800.jpg 800w, card-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw,
(max-width: 900px) 50vw,
400px"
alt="Article card image"
>
Read sizes top-to-bottom — the first matching condition wins. The last value is the default (no media condition).
2.4 The <picture> element (art direction)
srcset + sizes handle resolution switching (same image, different file sizes). The <picture> element handles art direction — different image crops or entirely different images for different viewports.
<picture>
<!-- Wide screen: landscape crop -->
<source media="(min-width: 800px)" srcset="hero-wide.jpg">
<!-- Medium screen: square crop -->
<source media="(min-width: 500px)" srcset="hero-square.jpg">
<!-- Fallback (also used by screen readers) -->
<img src="hero-portrait.jpg" alt="Woman coding at a desk">
</picture>
Always include an <img> as the last child — it is the fallback and the source of the alt text.
2.5 Modern image formats with <picture>
You can use <picture> to serve next-generation formats with a JPEG/PNG fallback:
<picture>
<source type="image/avif" srcset="photo.avif">
<source type="image/webp" srcset="photo.webp">
<img src="photo.jpg" alt="A sunset over the ocean">
</picture>
| Format | Support | Compression vs JPEG | Best for |
|---|---|---|---|
| JPEG | Universal | Baseline | Photos (fallback) |
| WebP | All modern browsers | ~30% smaller | Photos and illustrations |
| AVIF | Chrome, Firefox, Safari 16+ | ~50% smaller | Photos where quality matters |
| PNG | Universal | Lossless | Logos, screenshots with text |
| SVG | Universal | Vector (no raster) | Icons, logos, illustrations |
2.6 Fluid images with CSS
The simplest way to keep images within their container on any screen:
img {
max-width: 100%;
height: auto; /* preserves aspect ratio */
display: block; /* removes the inline baseline gap */
}
2.7 The aspect-ratio property
aspect-ratio locks the width-to-height ratio of any element:
.video-wrapper {
aspect-ratio: 16 / 9;
width: 100%;
}
.avatar {
aspect-ratio: 1; /* shorthand for 1 / 1 (square) */
width: 60px;
border-radius: 50%;
}
This replaces the old "padding-top hack" for maintaining ratios.
2.8 object-fit and object-position
When you set explicit dimensions on an <img> or <video>, object-fit controls how the content fills the box:
| Value | Behaviour |
|---|---|
fill | Stretches to fill — distorts if aspect ratio differs (default) |
contain | Scales to fit inside — may leave empty space (letterbox) |
cover | Scales to cover — may crop edges (most common) |
none | Natural size, not resized |
scale-down | Smallest of none or contain |
.card-image {
width: 100%;
height: 200px;
object-fit: cover;
object-position: center top; /* focus on the top of the image */
}
2.9 Fluid typography with clamp()
clamp(min, preferred, max) returns a value clamped between a minimum and maximum.
/* Font size: minimum 1rem, preferred 4vw, maximum 2.5rem */
h1 {
font-size: clamp(1rem, 4vw, 2.5rem);
}
p {
font-size: clamp(0.9rem, 1.5vw + 0.5rem, 1.125rem);
}
The formula slope * viewport + intercept inside clamp() gives a perfectly linear scale between two breakpoints:
/* Grows from 1rem at 320px viewport to 2rem at 1200px viewport */
/* slope = (2 - 1) / (1200 - 320) = 0.001136 → 0.1136vw per px, ×100 = 11.36px per 100vw */
/* Or use an online fluid type calculator */
font-size: clamp(1rem, 0.5rem + 1.5625vw, 2rem);
2.10 Lazy loading
The loading="lazy" attribute defers off-screen images until the user scrolls near them — zero JavaScript required:
<img src="photo.jpg" alt="..." loading="lazy" width="800" height="600">
Always add width and height attributes. The browser uses them to reserve layout space before the image loads, preventing layout shift (CLS).
3. Real World Example
A news article page uses responsive images and fluid typography to deliver a great reading experience on every device:
- Hero image —
<picture>with AVIF/WebP sources and a landscape-vs-portrait art-direction switch. - Article body images —
srcset+sizes="(max-width: 700px) 100vw, 700px"ensures images never download wider than the article column. - Headlines —
clamp(1.5rem, 5vw, 3rem)so they look bold on desktop and compact on mobile without a media query. - Author avatar —
aspect-ratio: 1; object-fit: cover; border-radius: 50%— a perfect circle whatever the source image dimensions. - Embedded video —
aspect-ratio: 16 / 9; width: 100%replaces the old padding-top: 56.25% hack.
4. Code Example
<!-- ============================================================
RESPONSIVE IMAGES & TYPOGRAPHY — COMPLETE EXAMPLE
============================================================ -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Images Demo</title>
<style>
/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
img, video {
max-width: 100%;
height: auto;
display: block;
}
/* ---- Fluid Typography ---- */
body { font-size: clamp(1rem, 0.9rem + 0.5vw, 1.125rem); }
h1 { font-size: clamp(1.75rem, 5vw, 3rem); }
h2 { font-size: clamp(1.25rem, 3vw, 2rem); }
/* ---- Layout ---- */
.container {
max-width: 900px;
margin: 0 auto;
padding: 1rem;
}
/* ---- Hero ---- */
.hero-img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
/* ---- Card Grid ---- */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.card-thumb {
width: 100%;
height: 180px;
object-fit: cover;
object-position: center;
}
.card-body { padding: 1rem; }
/* ---- Avatar ---- */
.avatar {
width: 64px;
aspect-ratio: 1;
border-radius: 50%;
object-fit: cover;
}
/* ---- Video wrapper ---- */
.video-embed {
width: 100%;
aspect-ratio: 16 / 9;
}
.video-embed iframe {
width: 100%;
height: 100%;
border: 0;
}
</style>
</head>
<body>
<div class="container">
<!-- Fluid headline -->
<h1>Responsive Images & Typography</h1>
<!-- Art direction: different crop on mobile vs desktop -->
<picture>
<source
media="(min-width: 800px)"
type="image/webp"
srcset="hero-wide.webp"
>
<source
media="(min-width: 800px)"
srcset="hero-wide.jpg"
>
<source type="image/webp" srcset="hero-portrait.webp">
<img
class="hero-img"
src="hero-portrait.jpg"
alt="Sunrise over mountain peaks"
width="900"
height="506"
loading="eager"
>
</picture>
<!-- Author row with circular avatar -->
<img
class="avatar"
src="avatar.jpg"
alt="Jane Doe"
width="64"
height="64"
>
<!-- Card grid — resolution switching with srcset/sizes -->
<div class="card-grid">
<div class="card">
<img
class="card-thumb"
src="photo-800.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w"
sizes="(max-width: 600px) 100vw, (max-width: 900px) 50vw, 300px"
alt="Mountain trail"
width="800"
height="600"
loading="lazy"
>
<div class="card-body">
<h2>Mountain Trail</h2>
<p>A beginner-friendly loop with great views.</p>
</div>
</div>
<div class="card">
<picture>
<source type="image/avif" srcset="forest.avif">
<source type="image/webp" srcset="forest.webp">
<img
class="card-thumb"
src="forest.jpg"
alt="Dense forest path"
width="800"
height="600"
loading="lazy"
>
</picture>
<div class="card-body">
<h2>Forest Walk</h2>
<p>Cool shade and birdsong all the way.</p>
</div>
</div>
</div>
<!-- Responsive video embed -->
<div class="video-embed">
<iframe
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
title="Demo video"
allowfullscreen
></iframe>
</div>
</div>
</body>
</html>
5. Code Breakdown
Fluid typography (lines 14–16)
clamp(1rem, 0.9rem + 0.5vw, 1.125rem) — body text grows from 1 rem at tiny screens to 1.125 rem on wide screens. The middle value blends a fixed offset (0.9rem) with a viewport-relative amount (0.5vw). This gives a smooth curve rather than a sudden jump.
Global img reset (lines 7–10)
max-width: 100% prevents images from overflowing their containers. height: auto keeps the original aspect ratio. display: block removes the 3–4 px baseline gap below inline images.
aspect-ratio (line 34)
aspect-ratio: 16 / 9 on the hero image replaces the old padding-top: 56.25% hack. The browser maintains the 16:9 ratio as the width changes. Combined with object-fit: cover, the image always fills the box without distortion.
srcset + sizes (cards, lines 82–89)
The browser knows the image has candidates at 400 w and 800 w. The sizes attribute tells it the rendered width: 100vw on mobile, 50vw on tablets, and 300 px on wide screens. On a 375 px phone at 2× DPR the browser needs 375 × 2 = 750 px — it picks photo-800.jpg. On a 300 px column on desktop it picks photo-400.jpg.
Art direction with <picture> (hero, lines 42–57)
On screens 800 px and wider the browser uses the landscape crop. On smaller screens it falls back to the portrait crop. Within each branch, WebP is offered first with a JPEG fallback for older browsers. The <img> fallback is always required.
loading="lazy" (line 88)
Off-screen images defer their network request until the user scrolls near them. The hero image uses loading="eager" (or omits the attribute) so it loads immediately — it is above the fold.
width and height attributes
Setting width and height on every <img> lets the browser compute aspect-ratio from those values before the image loads, reserving the correct space and preventing layout shift.
6. Common Mistakes
Mistake 1 — Omitting sizes with srcset
Without sizes, the browser assumes the image fills the entire viewport (100vw) and may download a much larger file than needed.
/* Bad — browser assumes 100vw for a 300px card image */
<img src="card.jpg" srcset="card-400.jpg 400w, card-800.jpg 800w" alt="...">
/* Good */
<img src="card.jpg"
srcset="card-400.jpg 400w, card-800.jpg 800w"
sizes="(max-width: 700px) 100vw, 300px"
alt="...">
Mistake 2 — Missing <img> fallback inside <picture>
/* Bad — no fallback, no alt text */
<picture>
<source srcset="hero.webp" type="image/webp">
</picture>
/* Good */
<picture>
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Descriptive text here">
</picture>
Mistake 3 — Using object-fit without explicit dimensions
/* Bad — object-fit has nothing to work with */
.card-img { object-fit: cover; }
/* Good — explicit height gives the property meaning */
.card-img { width: 100%; height: 200px; object-fit: cover; }
Mistake 4 — clamp() with unitless middle value
/* Bad — mixing vw with unitless number causes invalid calc */
font-size: clamp(1rem, 4, 2rem);
/* Good */
font-size: clamp(1rem, 4vw, 2rem);
Mistake 5 — Lazy-loading above-the-fold images
/* Bad — hero image is immediately visible; lazy delays it */
<img src="hero.jpg" loading="lazy" alt="...">
/* Good — eager (or omit the attribute) for LCP images */
<img src="hero.jpg" loading="eager" alt="...">
Mistake 6 — Forgetting width/height attributes
Without width and height, the browser cannot reserve layout space before the image loads, causing Cumulative Layout Shift (CLS) — a Core Web Vitals penalty.
7. Best Practices
- Add width and height attributes to every
<img>— even if you override them with CSS — to prevent layout shift. - Offer WebP/AVIF first inside
<picture>with a JPEG/PNG fallback for maximum compression savings. - Use srcset + sizes for resolution switching (same image, multiple sizes) and
<picture>only when you need different crops or formats. - Lazy-load below-the-fold images (
loading="lazy") and eager-load LCP/hero images. - Apply object-fit: cover to images inside fixed-size containers so they crop gracefully instead of distorting.
- Use aspect-ratio instead of the padding-top hack for responsive video embeds and fixed-ratio boxes.
- Write fluid type with clamp() — one rule replaces multiple breakpoints for headings and body text.
- Use SVG for icons and logos — they scale perfectly to any resolution with a tiny file size.
- Compress images before deployment. Tools: Squoosh, ImageOptim, Sharp (Node.js).
- Set object-position when the important part of an image is not in the center (e.g., a face near the top).
8. Practice Exercise
Build a responsive photo blog card component.
Requirements
- Create an HTML file with three article cards laid out in a CSS Grid (
auto-fill, minmax(280px, 1fr)). - Each card has a thumbnail image area (200 px tall) that uses
object-fit: cover. - For at least one card, wrap the image in
<picture>and provide a WebP source plus a JPEG fallback. - Add
srcset(400 w and 800 w) and appropriatesizesto the other card images. - Make the card title use
clamp(1.1rem, 3vw, 1.5rem). - Add
loading="lazy"to all card images. - Include
widthandheightattributes on every<img>. - Add a hero banner at the top that maintains a 16:9 aspect ratio using the
aspect-ratioproperty.
Bonus
- Add
prefers-color-scheme: darksupport — swap to darker card backgrounds. - Use
<picture>with amediaattribute to show a portrait crop of the hero on mobile. - Write a fluid type scale for h1, h2, and body using only
clamp()— no breakpoints needed.
9. Assignment
Audit and upgrade an existing page for responsive images and fluid typography.
- Take any static HTML page you built in a previous module (or create a simple one).
- Replace every plain
<img src>with eithersrcset/sizesor<picture>— choose whichever is appropriate. - Add
loading="lazy"to all images except the first above-the-fold image. - Ensure all images have
width,height, andaltattributes. - Replace all fixed
font-sizevalues on headings withclamp()equivalents. - Convert any fixed-height image containers to use
object-fit: cover. - Replace any
padding-topaspect-ratio hacks with theaspect-ratioproperty. - Open DevTools Network tab, throttle to "Slow 3G", and verify images lazy-load as you scroll.
Deliverable: A single HTML file with embedded CSS. Write a short HTML comment at the top explaining which techniques you applied and why.
10. Interview Questions
- What is the difference between srcset and the <picture> element?
srcset (with sizes) handles resolution switching — the same image at different file sizes. <picture> handles art direction — completely different images or crops for different viewports. - What does the w descriptor in srcset mean?
It is the intrinsic width of the image file in pixels. The browser combines it with the sizes value and device pixel ratio to select the best candidate. - Why should you always include width and height on <img> elements?
The browser uses them to compute the aspect ratio and reserve layout space before the image downloads, preventing Cumulative Layout Shift (CLS). - What does object-fit: cover do?
It scales the image so it completely fills the container, cropping any overflow, while preserving the original aspect ratio. No distortion occurs. - How does clamp() enable fluid typography?
clamp(min, preferred, max) scales between the minimum and maximum based on the preferred value — usually a viewport-relative unit like vw. The font grows smoothly as the viewport widens, staying within safe bounds. - When should you NOT use loading="lazy"?
On the Largest Contentful Paint (LCP) image — usually the hero or first above-the-fold image. Lazy-loading it delays the most important visible image and hurts performance scores. - What is the aspect-ratio property and what did we use before it?
aspect-ratio sets a preferred width-to-height ratio on an element. Before it, the common workaround was setting padding-top to a percentage (e.g., padding-top: 56.25% for 16:9) on a position:relative container. - What are WebP and AVIF and why would you use them?
Modern image formats offering 30–50% smaller file sizes than JPEG at equivalent quality. WebP has near-universal browser support; AVIF compresses even better but has slightly less support. Use <picture> to serve them with a JPEG fallback.
11. Additional Resources
- MDN — Responsive images guide — in-depth explanation of srcset, sizes, and <picture>
- MDN — aspect-ratio — browser support, syntax, use-cases
- MDN — clamp() — full reference with examples
- web.dev — Serve images in modern formats — Google's guidance on WebP and AVIF
- web.dev — Optimize Cumulative Layout Shift — why width/height attributes matter
- Squoosh — free in-browser tool to convert and compress images to WebP/AVIF
- utopia.fyi/type — fluid type scale calculator (generates clamp() values for you)
- CSS Tricks — A complete guide to CSS object-fit and object-position