Step 05 of 21
Responsive design
Make layouts that adapt from phones to wide monitors using fluid units, media queries, container queries and responsive images.
Your page will be viewed on a 360px-wide phone, a tablet turned sideways, a laptop, and a 2560px monitor – sometimes by the same person on the same day. Responsive design means building one page that works well on all of them, rather than separate "mobile" and "desktop" sites. This lesson starts with fluid foundations that adapt automatically, then adds media queries for page-level changes and container queries for components that adapt to wherever they are placed.
The viewport meta tag
When smartphones arrived, most sites were designed for desktops. So mobile browsers pretended to be a desktop: they laid the page out on a virtual canvas around 980px wide and shrank it to fit the screen. Text became unreadably small.
To opt out of that behaviour, every responsive page needs this tag in its <head>:
<meta name="viewport" content="width=device-width, initial-scale=1">It tells the browser to make the layout viewport as wide as the device's screen in CSS pixels (around 360–430px on most phones) and not to zoom. Without it, none of your media queries will behave as expected on mobile. Never add maximum-scale=1 or user-scalable=no: they stop people zooming in to read.
Fluid foundations
The most robust responsive layouts need surprisingly few breakpoints, because they are built from things that stretch and shrink on their own.
Relative units
| Unit | Relative to | Good for |
|---|---|---|
% | The parent's size | Widths inside a container |
rem | The root element's font size (16px by default) | Font sizes, spacing, breakpoints |
em | The current element's font size | Padding that should scale with a button's text |
vw / vh | 1% of the viewport's width / height | Full-bleed sections, fluid type (combined with rem) |
dvh / svh / lvh | Dynamic / smallest / largest viewport height | Full-height sections on mobile |
ch | The width of the "0" character | Readable line lengths |
Using rem for font sizes and spacing means your layout scales when a user increases their browser's default font size – a setting many people with low vision rely on.
Containers that never overflow
Fixed widths are the most common cause of horizontal scrolling on phones. Instead, cap a width and let it shrink:
.container {
width: min(100% - 2rem, 70rem); /* 1rem gutter each side, max 70rem */
margin-inline: auto; /* centre it */
}
.prose {
max-width: 65ch; /* comfortable reading line length */
}
img, video {
max-width: 100%; /* never wider than their container */
height: auto; /* keep the aspect ratio */
display: block;
}min() picks the smaller of its arguments, so the container is full width minus gutters on a phone and 70rem on a big screen – no media query needed.
Fluid typography with clamp()
clamp(MIN, PREFERRED, MAX) returns the preferred value but never goes below the minimum or above the maximum:
h1 {
font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem);
}At a 320px viewport, 1.2rem + 2.5vw is 19.2px + 8px = 27.2px, so the minimum (28px) applies. At 1280px it would be 51.2px, so the maximum (48px) applies. In between, the heading grows smoothly.
Intrinsic layouts
Layout techniques from the previous lesson are responsive by design: flex-wrap: wrap moves items onto new lines when space runs out, and repeat(auto-fit, minmax(min(16rem, 100%), 1fr)) changes the number of grid columns automatically. Reach for these first, and use queries for the changes they cannot express.
Media queries
A media query applies CSS only when a condition about the device or viewport is true:
@media (min-width: 48rem) {
.site-nav { flex-direction: row; }
}You can combine conditions with and, list alternatives with a comma, and use the modern range syntax, which all evergreen browsers support:
@media (min-width: 48rem) and (max-width: 63.99rem) { /* tablets only */ }
@media (48rem <= width < 64rem) { /* range syntax: no .99 hack */ }
@media (orientation: landscape), (min-width: 64rem) { /* either condition */ }Mobile first
Write your base styles for the smallest screen, then use min-width queries to add complexity as space grows:
/* Base: one column, stacked nav – works everywhere */
.site-nav { display: flex; flex-direction: column; gap: 0.5rem; }
.layout { display: grid; gap: 1.5rem; }
/* Medium screens and up */
@media (min-width: 48rem) {
.site-nav { flex-direction: row; gap: 1.5rem; }
}
/* Large screens and up: add a sidebar */
@media (min-width: 64rem) {
.layout { grid-template-columns: 16rem minmax(0, 1fr); }
}Mobile-first CSS tends to be shorter: small screens get the simplest styles and never apply overrides they do not need, and each query only adds. Desktop-first CSS with max-width queries spends its effort undoing styles.
Choosing breakpoints
Do not pick breakpoints from a list of popular devices – there are thousands of screen sizes. Instead, start with the narrow layout, widen the browser slowly, and add a breakpoint where the design starts to look wrong: lines become too long, cards too stretched, the nav too cramped. Most sites need only two or three.
Write breakpoints in rem or em. In media queries these units are based on the browser's default font size, so if a user sets a larger default text size, your layout switches to the narrower design earlier – exactly what they need.
Beyond width: user preference features
Media queries can also respond to how a person uses their device:
/* Respect users who get motion sickness from animation */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Dark mode, following the operating system setting */
@media (prefers-color-scheme: dark) {
:root { --bg: #0f172a; --text: #e2e8f0; }
}
/* Only show hover effects on devices that can actually hover */
@media (hover: hover) {
.card:hover { transform: translateY(-2px); }
}(pointer: coarse) matches touchscreens, where tap targets should be generously sized. Remember that screen width says nothing about input: a small laptop window still has a mouse, and a large tablet still has fingers.
Container queries
Media queries ask about the viewport. But a product card might appear in a wide main column on one page and a narrow sidebar on another, at the same viewport width. What the card really cares about is the width of the space it has been given. Container queries answer exactly that.
<div class="card-slot">
<article class="product">
<img src="/img/shoe.jpg" alt="Grey trail shoe">
<div class="product-body">
<h3>Ridge Runner 3</h3>
<p>Our lightest waterproof shoe.</p>
</div>
</article>
</div>.card-slot {
container-type: inline-size; /* make this element a query container */
}
.product {
display: grid;
gap: 1rem;
}
@container (min-width: 28rem) {
.product {
grid-template-columns: 10rem 1fr; /* image beside text when there is room */
}
}Now the card stacks vertically in a narrow slot and switches to a side-by-side layout in a wide one, wherever it is used.
Things to know:
- An element cannot query itself. The query looks up to the nearest ancestor container, which is why the example has a wrapper element. Styles inside
@containerapply to descendants of the container. - Name containers when they are nested:
container: card / inline-size;then@container card (min-width: 28rem). inline-sizecontainment means the container's width cannot depend on its content. Put it on elements that get their width from their parent, such as block elements or grid cells. On a shrink-to-fit element (like a float or aninline-block) it can collapse to zero width.- Container query units:
cqiis 1% of the container's inline size, sofont-size: clamp(1rem, 4cqi, 1.5rem)scales text with the component.
Responsive images
max-width: 100% makes an image fit, but a phone still downloads the full desktop-sized file. srcset and sizes let the browser pick a suitable file:
<img
src="/img/hero-800.jpg"
srcset="/img/hero-400.jpg 400w, /img/hero-800.jpg 800w, /img/hero-1600.jpg 1600w"
sizes="(min-width: 64rem) 50vw, 100vw"
alt="Runner on a ridge at sunrise"
width="1600" height="900">srcset lists the files and their actual widths (w). sizes tells the browser how wide the image will be displayed: half the viewport on large screens, full width otherwise. The browser combines that with the screen's pixel density and chooses the smallest file that will look sharp.
When you need a different crop on small screens – not just a smaller file – use <picture>:
<picture>
<source media="(min-width: 48rem)" srcset="/img/team-wide.jpg">
<img src="/img/team-square.jpg" alt="The Ridgeway team outside the shop">
</picture>Testing and common pitfalls
Use the device toolbar in Chrome DevTools or Responsive Design Mode in Firefox DevTools to drag the viewport through every width, not just a few device presets. Then check on a real phone, and zoom to 200% on desktop.
- Horizontal scrolling almost always comes from a fixed width, a long unbreakable string, or an image without
max-width. In DevTools, look for the element that extends past the right edge. 100vhon mobile includes the area behind the browser's address bar, so content gets hidden. Usemin-height: 100dvh.- Hover-only interactions – menus that open on hover – do not work on touchscreens. Make them open on click or tap too.
- Hiding content on mobile with
display: noneoften removes things phone users need. Rearrange instead.
Practice
Build a page with a reusable profile card and a responsive layout:
- A
.layoutwith a<main>and an<aside>. Mobile first: stacked on small screens; from64remup, main on the left and a 20rem aside on the right. - A profile card (avatar image, name, short bio) wrapped in a container. Place one copy in
<main>and one in<aside>. - The card stacks vertically by default and shows the avatar beside the text when its container is at least 26rem wide.
- The page title uses fluid type between 1.75rem and 3rem.
- Hover effects on the card only apply on devices that can hover.
Resize the window: at large widths the card in <main> should be side by side while the card in the narrow <aside> stays stacked.
Show solution
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Responsive practice</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; line-height: 1.5; }
img { max-width: 100%; height: auto; display: block; }
.container { width: min(100% - 2rem, 75rem); margin-inline: auto; }
h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); }
.layout { display: grid; gap: 1.5rem; }
@media (min-width: 64rem) {
.layout { grid-template-columns: minmax(0, 1fr) 20rem; }
}
.card-slot { container-type: inline-size; }
.profile {
display: grid;
gap: 1rem;
padding: 1rem;
border: 1px solid #cbd5e1;
border-radius: 12px;
transition: box-shadow 0.2s;
}
.profile img { width: 6rem; border-radius: 50%; }
@container (min-width: 26rem) {
.profile { grid-template-columns: auto 1fr; align-items: center; }
}
@media (hover: hover) {
.profile:hover { box-shadow: 0 4px 16px rgb(0 0 0 / 0.12); }
}
</style>
</head>
<body>
<div class="container">
<h1>Our team</h1>
<div class="layout">
<main>
<div class="card-slot">
<article class="profile">
<img src="avatar.jpg" alt="" width="192" height="192">
<div>
<h2>Amara Okafor</h2>
<p>Leads our trail-running clinics and tests every shoe we sell.</p>
</div>
</article>
</div>
</main>
<aside>
<div class="card-slot">
<article class="profile">
<img src="avatar.jpg" alt="" width="192" height="192">
<div>
<h2>Leo Martins</h2>
<p>Runs the repair workshop.</p>
</div>
</article>
</div>
</aside>
</div>
</div>
</body>
</html>The avatars use alt="" because the name next to them already identifies the person. From 64rem up, the aside is 20rem wide, so its card container never reaches 26rem and the card stays stacked, while the main column is far wider and its card sits side by side. Below 64rem both cards are full width, so they switch together depending on the window width – each card only ever looks at its own container. Save any square image as avatar.jpg next to the HTML file.
Key takeaways
- Every responsive page needs
<meta name="viewport" content="width=device-width, initial-scale=1">. - Start fluid: relative units,
min()containers,max-width: 100%images andclamp()type remove most breakpoints. - Write mobile-first CSS and add
min-widthmedia queries, inrem, where the content needs them. - Media features such as
prefers-reduced-motion,prefers-color-schemeandhoverrespond to how people use their device. - Container queries let components adapt to the space they are given; the container must be an ancestor with
container-type. srcsetandsizesserve appropriately sized images;<picture>handles different crops.
Check yourself
Quiz
0/4 correct · 4 left
1.A page looks tiny and zoomed out on phones, and none of its media queries seem to apply. What is most likely missing?
2.Why is font-size: clamp(1rem, 0.8rem + 1vw, 1.5rem) better than font-size: 3vw?
3.You add @container (min-width: 30rem) { .card { … } } but the card never changes. Which is the most likely cause?
4.In mobile-first CSS, how are styles for larger screens usually added?
Finished reading?
Mark it complete to fill in your roadmap.