Responsive Design
Pages that work on every screen: the viewport, fluid layouts, mobile-first media queries, breakpoints, container queries, fluid type with clamp(), and responsive images.
What responsive design is
Responsive web design
is the practice of building one page whose layout, text and images
adapt to the size of the screen or window, instead of building a
separate mobile site. The term was coined by Ethan Marcotte in 2010,
who described three ingredients: fluid grids, flexible images and
media queries. Today flexbox, grid, container queries and functions
such as clamp() have been added to the toolbox, but the
idea is the same.
It is also an accessibility requirement. WCAG success criterion 1.4.10 Reflow (level AA) asks that content can be used at a width of 320 CSS pixels without scrolling in two directions (apart from things like maps and data tables that need both). 320 CSS pixels is what a 1280-pixel-wide window shows when a user zooms in to 400%, so a page that reflows well on a small phone also works for people who enlarge the page. Criterion 1.4.4 Resize Text (AA) asks that text can be enlarged to 200% without loss of content.
The viewport
The viewport is the area of the browser window in
which the page is shown. On a desktop it is the window minus the
toolbars; on a phone it is the screen. Without instructions, mobile
browsers pretend to have a wide viewport (typically 980 CSS pixels),
lay the page out for that, and shrink the result to fit the screen,
so the text becomes tiny. The
viewport meta tag in the
<head> switches that off:
<meta name="viewport" content="width=device-width, initial-scale=1">
width=device-width makes the layout viewport as wide as
the device in CSS pixels, and initial-scale=1 starts
without zoom. Leave out maximum-scale=1 and
user-scalable=no: they stop people from zooming in,
which fails users who need larger text.
CSS pixels are not device pixels. A phone with a 1080-pixel-wide panel typically reports a viewport of roughly 360 to 410 CSS pixels, and draws each CSS pixel with several device pixels (see pixel density). Responsive CSS always works in CSS pixels, so you never need to know the physical resolution.
Viewport units and the mobile toolbar
vw and vh are 1% of the viewport's width
and height (see
Colors, Units and Typography). On phones, 100vh has a known problem: the address
bar appears and disappears while scrolling, and
100vh is the height without the bar, so a
"full screen" section is cut off at the bottom. The newer units
solve this: svh (small, toolbar shown),
lvh (large, toolbar hidden) and dvh
(dynamic, follows the toolbar). They have been widely available
since June 2025.
.hero {
min-height: 100svh; /* always fits, even with the toolbar visible */
}
Fluid layouts
A fluid layout uses flexible sizes (percentages,
fr, flexbox, auto) instead of fixed pixel
widths, so it fits any width by itself. Most of the work of a
responsive page happens here, before any media query:
-
Give containers a
max-widthinstead of awidth, so they shrink on small screens and stop growing on large ones. -
Let images and other media shrink:
img, svg, video { max-width: 100%; height: auto; }. -
Use layouts that adapt themselves, such as flexbox with
flex-wrap(see Flexbox) and grid withrepeat(auto-fit, minmax(…))(see CSS Grid). -
Use
min()andmax(), which pick the smaller or larger of their values:width: min(100% - 2rem, 30rem)means "30rem, but never wider than the screen minus a 1rem margin on each side".
<div class="wrap">
<h2>Outage map</h2>
<svg viewBox="0 0 600 200" role="img" aria-label="Map placeholder with three outage areas">
<rect width="600" height="200" fill="#e8f0e8"></rect>
<circle cx="140" cy="90" r="34" fill="#b03a2e"></circle>
<circle cx="330" cy="130" r="22" fill="#b03a2e"></circle>
<circle cx="470" cy="70" r="28" fill="#b03a2e"></circle>
</svg>
<p>Three areas without power. The box and the map scale with the screen.</p>
</div>
.wrap {
box-sizing: border-box;
width: min(100% - 2rem, 30rem); /* 30rem, or less on small screens */
margin-inline: auto; /* center horizontally */
padding: 1rem;
border: 1px solid #555;
}
.wrap h2 {
margin: 0 0 0.5rem;
}
.wrap svg {
display: block;
width: 100%; /* as wide as the box */
height: auto; /* height follows from the viewBox ratio */
}
In the content column of this page the box is 30rem wide and centered. On a narrow phone it takes the full width minus 1rem on each side, and the map scales down with it, keeping its proportions. No media query is involved.
Media queries
A media query applies CSS only when a condition about the device or the viewport is true. The most used condition is the viewport width:
/* Range syntax (widely available since September 2025) */
@media (width >= 40rem) {
.layout { grid-template-columns: 2fr 1fr; }
}
/* The same in the older syntax, which works everywhere */
@media (min-width: 40rem) {
.layout { grid-template-columns: 2fr 1fr; }
}
The rules inside the block take part in the cascade like any other
rules; the media query only decides whether they count. The newer
range syntax (width >= 40rem,
30rem <= width < 60rem) is easier to read than
min-width/max-width; it reached all major
browsers in March 2023. You will see both.
The Kestrel outage page shows the current outages and the planned work in one column on phones and side by side when there is room. The frame is as wide as this page's content column, so on a desktop you see two columns; make the window narrow, as on a phone, to see the layout stack:
<div class="layout">
<main>
<h2>Current outages</h2>
<p>Three areas without power in Den Haag and Delft.</p>
</main>
<aside>
<h2>Planned work</h2>
<p>Leiden, tonight 22:00–02:00.</p>
</aside>
</div>
.layout {
display: grid;
gap: 1rem;
}
.layout > * {
padding: 0.75rem;
border: 1px solid #555;
}
.layout h2 {
margin: 0 0 0.25rem;
font-size: 1.125rem;
}
.layout p {
margin: 0;
}
@media (width >= 30rem) {
.layout {
grid-template-columns: 2fr 1fr;
}
}
Media types and other features
A media query can also test the media type (screen
or print; all is the default) and many
other media features:
| Query | True when |
|---|---|
@media print |
The page is printed or saved as PDF |
(orientation: landscape) |
The viewport is wider than it is tall |
(hover: hover) |
The main input device can hover (a mouse); false on most touch screens |
(pointer: coarse) |
The main pointer is imprecise, such as a finger |
(prefers-reduced-motion: reduce) |
The user asked the system for less motion (see Transitions and Animations) |
(prefers-color-scheme: dark) |
The user chose a dark theme (see Dark Mode and Theming) |
hover and pointer are better than a width
check for deciding whether to rely on hover effects or to make tap
targets larger: a small laptop window is not a touch screen, and a
large tablet is.
Mobile first and breakpoints
Mobile-first design
means writing the base CSS for the smallest screens and adding
min-width (or width >=) media queries
that enhance the layout as space grows. The opposite, desktop-first,
starts with the wide layout and uses max-width queries
to undo parts of it.
Mobile first has practical advantages:
- The small-screen layout is usually the simple one: a single column in source order, which needs almost no CSS. Wider layouts add columns instead of removing them.
- It forces you to decide what matters most, because a phone has no room for everything at once.
- Browsers that do not understand a query simply get the simple, working layout.
A responsive breakpoint is a width at which a media query changes the layout. Choose breakpoints where your content starts to look wrong (lines too long, columns too squeezed), not at the screen widths of particular devices: there are thousands of devices, and new ones appear every year. Two or three breakpoints are enough for most pages.
min-width query adds to them from its breakpoint
upward. At 70rem all three layers apply, and the last one wins
where they conflict.
A navigation menu written mobile first: full-width links that are easy to tap, which become a row when there is room.
<nav aria-label="Portal">
<ul class="menu">
<li><a href="#">Outages</a></li>
<li><a href="#">Meter readings</a></li>
<li><a href="#">Invoices</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
/* Base: small screens */
.menu {
display: grid;
gap: 0.25rem;
margin: 0;
padding: 0;
list-style: none;
}
.menu a {
display: block;
padding: 0.75rem 1rem; /* large tap targets */
border: 1px solid #555;
color: #1f4e79;
}
/* Enhancement: wider screens */
@media (width >= 30rem) {
.menu {
display: flex;
flex-wrap: wrap;
}
.menu a {
padding: 0.5rem 1rem;
}
}
Write breakpoints in rem or em
A breakpoint in rem or em moves with the
user's default font size. In a media query, both units refer to the
browser's initial font size (usually 16px), not to a font size you
set on <html>, so 40rem is 640px for
most people. Someone who set their browser to a larger font gets the
narrower layout sooner, which is exactly what their larger text
needs. A breakpoint in px ignores that setting.
Container queries
Media queries look at the whole viewport. A component, however, does not know how much room it gets: the same card can sit in a wide main column or a narrow sidebar on the same screen. A container query applies styles based on the size of an ancestor element (the container) instead of the viewport, so components can adapt to where they are placed.
It takes two steps. First mark an element as a container with
container-type: inline-size (its width can be queried).
Then write an @container rule, which tests the nearest
container ancestor. In this example the same outage notice is placed
twice: once in a wide slot and once in a narrow one. Both are on the
same screen, yet they get different layouts:
<div class="slot">
<article class="notice">
<div class="icon" aria-hidden="true">!</div>
<div>
<h3>Outage in Delft</h3>
<p>Crew on site, power expected back at 16:30.</p>
</div>
</article>
</div>
<div class="slot narrow">
<article class="notice">
<div class="icon" aria-hidden="true">!</div>
<div>
<h3>Outage in Delft</h3>
<p>Crew on site, power expected back at 16:30.</p>
</div>
</article>
</div>
.slot {
container-type: inline-size; /* children can query this width */
margin-bottom: 1rem;
}
.narrow {
max-width: 14rem;
}
.notice {
display: grid;
gap: 0.5rem;
padding: 0.75rem;
border: 1px solid #555;
}
.notice h3,
.notice p {
margin: 0;
}
.icon {
display: grid;
place-items: center;
width: 2.5rem;
height: 2.5rem;
background: #b03a2e;
color: white;
font-weight: bold;
}
/* Icon next to the text when the container is at least 20rem wide */
@container (width >= 20rem) {
.notice {
grid-template-columns: 2.5rem 1fr;
align-items: start;
}
}
In the wide slot the icon sits next to the text; in the narrow slot
it sits above it. With a media query this is impossible, because
both cards share one viewport. Container queries also bring their
own units: cqi is 1% of the container's inline size
(its width, in horizontal text).
Fluid type with clamp()
clamp(min, preferred,
max)
returns the preferred value, but never less than the minimum and
never more than the maximum. It is shorthand for
max(min, min(preferred, max)) and works for any length:
font sizes, widths, padding, gaps.
Fluid typography uses it to let font sizes grow smoothly with the viewport, instead of jumping at breakpoints. The preferred value contains a viewport unit, and the minimum and maximum keep the text readable at both ends:
<h1 class="title">Outages in your area</h1>
<p>The heading grows with the width of the frame, between 1.5rem and 3rem.</p>
.title {
font-size: clamp(1.5rem, 1rem + 3vw, 3rem);
line-height: 1.2;
margin: 0 0 0.5rem;
}
Inside the frame, vw refers to the frame's own width.
In a frame 560px wide, 1rem + 3vw is 16px + 16.8px =
32.8px, between the limits of 24px and 48px. Resize the window and
the heading follows.
Responsive images
A layout that shrinks to a 360px phone should not make that phone
download a 2000-pixel photo. HTML has two tools for this, both
explained on
Links, Images and Media:
srcset and sizes let the browser pick a
file of the right resolution, and <picture> lets
you serve a different crop at different widths (art direction).
The responsive part is the sizes attribute: it tells
the browser, before the CSS has loaded, how wide the image will be
in the layout. It uses media conditions just like your CSS, so it
should match your breakpoints:
<img src="outage-map-800.webp"
srcset="outage-map-400.webp 400w,
outage-map-800.webp 800w,
outage-map-1600.webp 1600w"
sizes="(min-width: 60rem) 40rem, 100vw"
width="800" height="450"
alt="Map of Den Haag with outages in Laakkwartier, Scheveningen and Ypenburg">
Read sizes as: "on screens of 60rem and wider the image
is 40rem wide; otherwise it is the full viewport width". The browser
combines that with the screen's pixel density and picks the smallest
file that is sharp enough. The width and
height attributes give the image's aspect ratio, so the
browser reserves the right space before the file arrives and the
page does not jump. In CSS,
max-width: 100%; height: auto then lets it scale, and
aspect-ratio with object-fit: cover crops
an image to a fixed shape without distorting it.
Testing responsive pages
- Resize the window slowly from wide to narrow and watch for the moment something breaks: text overflowing, a horizontal scrollbar, columns that are too narrow. That is where a breakpoint belongs.
- Use device emulation in the browser's developer tools to try common phone sizes, touch input and portrait and landscape (see Debugging HTML and CSS with Developer Tools). Emulation does not reproduce a real phone's performance or toolbar behavior, so also test on at least one real device.
- Zoom to 400% in a desktop browser with a window about 1280px wide. Everything should still be usable without horizontal scrolling (WCAG 1.4.10 Reflow).
- Increase the default font size in the browser settings and check that text grows and layouts adapt.