Chapter 5
Responsive Design
Mobile-first CSS, media and container queries, fluid typography and responsive images.
5. Responsive Design
Responsive design is the practice of making an interface usable across a changing range of viewport sizes, input methods, and user settings. It is not a separate “mobile version” bolted onto a desktop page. A robust layout starts with content and behavior that work at the narrowest supported size, then adapts when the available space or capabilities call for a different presentation.
This chapter covers mobile-first styles, content-driven breakpoints, media and container queries, fluid type, responsive images, touch targets, and practical debugging techniques.
Overview & Objectives
By the end of this chapter, you will be able to:
- Explain the mobile-first approach and when to introduce a breakpoint.
- Write media queries using feature queries, range syntax, and capability preferences.
- Use container queries to adapt components to their actual space.
- Create fluid typography with
clamp()while preserving readable bounds. - Choose image candidates using
srcsetandsizes. - Use
<picture>for format selection and art direction. - Design usable touch targets and debug real mobile layouts systematically.
Mobile-First: Start with the Content
A mobile-first stylesheet begins with the most constrained layout, then adds enhancements as space becomes available:
.content-layout {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 1rem;
}
@media (min-width: 48rem) {
.content-layout {
grid-template-columns: minmax(0, 1fr) 18rem;
gap: 2rem;
}
}At a narrow width, the main content and complementary sidebar stack. Once there is enough room, they sit side by side. This direction usually keeps the base CSS simpler: essential content and functionality are present without requiring a large viewport or a sequence of overrides.
Mobile-first does not mean “design only for phones,” “hide information on small screens,” or “use a particular device breakpoint.” It means beginning with a layout that respects limited space and progressively adding arrangements when the content has room. A desktop-wide dashboard may have a different information density than a phone screen, but the user should not lose essential content or capabilities merely because the viewport is narrow.
Think through the content before writing a media query:
- What information and actions must remain available?
- What can naturally wrap, stack, or scroll within its own region?
- Which relationships change as space becomes available?
- Is the change caused by the viewport, or by a component's own allocated width?
- What happens with zoom, larger text, long translations, and on-screen browser controls?
Use a responsive viewport meta tag so mobile browsers lay out the page at the device width instead of an old desktop-sized virtual viewport:
<meta name="viewport" content="width=device-width, initial-scale=1">Do not disable zoom with user-scalable=no or restrictive maximum scales. Zoom supports users who need to magnify content.
Breakpoint Strategy
A breakpoint is a point where the layout needs a different rule, not a standard device category. Start with a fluid layout, then resize the page until a specific relationship stops working—for example, navigation collides, a paragraph becomes too wide, or two columns become cramped. Place the breakpoint where that content needs it.
Common strategies include:
- Content-driven breakpoints: Add a breakpoint when the current layout no longer fits or remains readable. This is usually the best default.
- Design-system breakpoints: Reuse a small set of project-wide thresholds when several components share the same layout transitions.
- Component-local breakpoints: Use container queries when an individual component needs to adapt to its own width, independent of the overall viewport.
Do not target a brand of phone or a device model. The same device can be rotated, split-screened, zoomed, or embedded in a narrow panel. A breakpoint chosen from content requirements works across those situations.
Media queries and range syntax
The familiar min-width form is mobile-first: styles inside it apply when the viewport is at least that wide.
.primary-nav {
display: grid;
gap: 0.5rem;
}
@media (min-width: 48rem) {
.primary-nav {
display: flex;
align-items: center;
}
}Modern media query range syntax supports direct comparisons:
@media (width >= 48rem) {
/* equivalent to a minimum-width query */
}
@media (width < 30rem) {
/* applies below 30rem */
}
@media (30rem <= width < 60rem) {
/* applies in a bounded range */
}The range form can make boundaries easier to read and lets adjacent ranges avoid overlapping. For broad browser support, min-width and max-width are still clear and widely understood; choose syntax according to your supported browsers and team conventions. If you need a compatibility fallback, write the traditional query first and the range form after it.
Media queries can respond to more than width:
@media (orientation: landscape) { /* viewport orientation */ }
@media (prefers-reduced-motion: reduce) { /* user motion preference */ }
@media (hover: hover) and (pointer: fine) { /* likely precise hover input */ }
@media (prefers-color-scheme: dark) { /* user color preference */ }Use capability queries to enhance or adapt interactions, not to assume that every person using a touchscreen has no pointer or that every desktop user has a mouse. Hybrid devices can support multiple input modes. Keep essential actions accessible without hover.
Avoid using orientation as a proxy for a particular device. A phone can be landscape, and a tablet can be portrait. Test both orientations when the design changes significantly.
Fluid Layouts Before Breakpoints
Many responsive layouts do not need a breakpoint for every width. Flexible tracks, intrinsic sizing, wrapping, and sensible maximum widths allow the browser to handle intermediate sizes:
.page {
width: min(100% - 2rem, 76rem);
margin-inline: auto;
}
.card-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1rem;
}The page has fluid side gutters but stops growing beyond a useful reading width. The card grid creates as many columns as fit, with a minimum track width that does not exceed the container on very narrow screens. These intrinsic layout techniques often eliminate awkward breakpoint-specific column counts.
Let text wrap naturally. Use min-width: 0 on flex or grid items when intrinsic content sizing prevents them from shrinking, and handle wide content—such as a data table—inside its own scrollable region when two-dimensional scrolling is necessary. Do not make the entire document horizontally scroll simply to preserve a desktop layout.
Container Queries: Responsive Components
A media query sees the viewport. A container query sees the size of a component's ancestor container. This matters when the same card is used in a wide main column and a narrow sidebar: the viewport might be unchanged even though the component has a different amount of space.
First establish a containment context:
.card-region {
container-type: inline-size;
container-name: card-region;
}Then query its inline size:
.product-card {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 1rem;
}
@container card-region (width >= 36rem) {
.product-card {
grid-template-columns: 10rem minmax(0, 1fr);
align-items: center;
}
}The component switches to a side-by-side presentation only when its named container is wide enough. The container must be an ancestor of the element being queried; an element cannot query its own size with a container query.
container-type: inline-size enables size queries along the inline axis and establishes containment that can affect intrinsic sizing. container-type: size adds containment in both dimensions; use it only when you genuinely want both axes contained, since it can change how an element's size is determined. Container names are optional if the intended ancestor is unambiguous:
.card-region { container: card-region / inline-size; }
@container (width > 40rem) { /* nearest eligible container */ }Container queries work well for reusable components, cards, embedded widgets, and modules placed in differently sized columns. Keep page composition in the page layout and component adaptation in the component. Avoid defining a container on the component itself when the rule needs to inspect the component's own width; put the containment context on a wrapper instead.
Fluid Typography with clamp()
Typography often needs to grow gradually between smaller and larger viewports. clamp(minimum, preferred, maximum) bounds a preferred fluid value:
.page-title {
font-size: clamp(2rem, 1.25rem + 2.5vw, 3.75rem);
}The browser calculates the preferred expression and clamps it to the minimum and maximum. Here, the title starts at 2rem, grows as the viewport grows, and stops at 3.75rem. The minimum remains a lower bound even on a very narrow screen.
A complete typographic scale might use:
:root {
--text-body: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
--text-heading: clamp(1.75rem, 1.3rem + 2vw, 3rem);
}
body { font-size: var(--text-body); }
h1 { font-size: var(--text-heading); }Avoid a pure viewport expression such as font-size: 2vw for body text. It can become too small on narrow screens and too large on wide ones; it can also fail to scale as expected when a user changes browser text settings. A rem-based minimum and maximum preserve bounds, while the fluid middle provides gradual change.
Check the actual computed size at minimum and maximum widths, at 200% zoom, and with larger default font settings. Fluid values can cause a heading to become too large or wrap awkwardly before reaching their cap. clamp() is a sizing tool, not a substitute for checking line length and hierarchy.
Responsive Images: Resolution Switching
Images can account for substantial network cost. Sending a large desktop image to a small display wastes bytes; sending a small image to a high-density screen can look blurry. srcset and sizes let the browser choose an appropriate candidate.
Width-descriptor srcset and sizes
<img
src="/images/field-guide-800.jpg"
srcset="
/images/field-guide-480.jpg 480w,
/images/field-guide-800.jpg 800w,
/images/field-guide-1400.jpg 1400w
"
sizes="(width < 50rem) calc(100vw - 2rem), 48rem"
width="1400"
height="900"
alt="A field guide open beside a notebook"
>The w descriptors state the intrinsic pixel width of each resource. sizes describes the expected rendered slot width; it does not force the image to that width. The browser combines the slot estimate, candidate widths, device pixel ratio, and other information to choose a resource.
In this example, below 50rem the image is expected to occupy the viewport width minus the side gutters; above that threshold it is expected to occupy 48rem. Keep sizes consistent with the actual CSS layout. If it says the image is 100vw when it is really a small card thumbnail, the browser may download an unnecessarily large candidate.
The src remains a fallback for browsers that do not use srcset. width and height communicate the image's intrinsic aspect ratio so the browser can reserve space before it loads, reducing layout shifts. CSS can still scale the image:
img {
max-width: 100%;
height: auto;
}Do not confuse srcset with fixed-density descriptors such as 1x and 2x. Width descriptors are usually more flexible for fluid layouts. Use one descriptor strategy per srcset, not a mixture of w and x.
Art direction and formats with <picture>
Resolution switching chooses among sizes of essentially the same image. Art direction selects a different crop or composition for a different viewport. <picture> can also offer modern formats with a fallback:
<picture>
<source
media="(width < 40rem)"
type="image/avif"
srcset="/images/runner-portrait.avif"
>
<source
media="(width < 40rem)"
type="image/webp"
srcset="/images/runner-portrait.webp"
>
<source
type="image/avif"
srcset="/images/runner-wide.avif"
>
<source
type="image/webp"
srcset="/images/runner-wide.webp"
>
<img
src="/images/runner-wide.jpg"
width="1600"
height="900"
alt="A runner crossing a misty mountain trail"
>
</picture>The browser evaluates <source> elements in order and uses a source whose media condition and format it supports. The <img> is the final fallback and provides the accessible alternative text. Keep source order deliberate: a matching supported source earlier in the list is selected.
For visually equivalent format alternatives, keep the same subject and aspect ratio when possible. For art-directed crops, the mobile crop may have different intrinsic dimensions; use CSS and source-specific aspect-ratio strategies where needed to avoid unexpected layout shifts. If the image is purely decorative, use alt=""; if the crop changes which information is conveyed, make sure the alternative text remains accurate for the selected image.
For responsive resolution switching across art-directed formats, each matching <source> may also have a srcset with width descriptors and a sizes attribute. Ensure all candidates within that source set represent the same crop and subject composition. A source set for a portrait crop must not mix candidates from the wide crop.
Choosing an image strategy
| Requirement | Technique |
|---|---|
| Same content, different pixel sizes | img with width-descriptor srcset and sizes |
| Different crop or composition by viewport | <picture> with media-qualified <source> elements |
| Prefer AVIF/WebP but retain a broadly supported fallback | <picture> with type sources and a final <img> |
| Decorative image conveys no information | Empty alt="" on the fallback <img> |
| Image affects layout before it loads | Supply intrinsic width and height |
Do not use CSS background images for meaningful content that needs alternative text or browser-native image semantics. Background images can still be appropriate for decoration.
Touch Targets and Input Flexibility
Touch users need controls that can be located and activated without precise pointer movement. WCAG 2.2 Success Criterion 2.5.8 specifies a minimum target size of 24 by 24 CSS pixels at Level AA, or sufficient spacing between undersized targets, subject to defined exceptions. WCAG 2.2 AAA has a larger 44 by 44 CSS pixel target criterion. These are conformance criteria, not guarantees that a control is comfortable for every person.
Aim for comfortable target areas, especially for primary actions and closely spaced controls:
.icon-button {
min-width: 2.75rem;
min-height: 2.75rem;
display: inline-grid;
place-items: center;
}The visible icon can remain small while the interactive target around it is larger. Leave enough spacing between adjacent controls so an imprecise tap does not activate the wrong action. Do not rely on hover as the only way to reveal functionality; keyboard and touch interactions often lack hover.
Do not infer touch solely from screen width. A narrow viewport may have a keyboard and mouse, and a large screen may be touch-enabled. Keep controls usable with mouse, touch, keyboard, and assistive technology rather than hiding essential affordances based on one input assumption.
Complete Example: Responsive Editorial Feature
This example combines a viewport-level page layout with a container-responsive feature card, fluid heading, and responsive image:
<main class="article-page">
<article>
<h1>Finding your first trail</h1>
<p>...</p>
<aside class="feature-region" aria-label="Featured route">
<article class="route-card">
<picture>
<source
media="(width < 42rem)"
type="image/avif"
srcset="/images/lake-crop.avif"
>
<source
media="(width < 42rem)"
type="image/webp"
srcset="/images/lake-crop.webp"
>
<source type="image/avif" srcset="/images/lake-wide.avif">
<source type="image/webp" srcset="/images/lake-wide.webp">
<img src="/images/lake-wide.jpg" width="1440" height="900" alt="A trail following a mountain lake">
</picture>
<div class="route-card__body">
<h2>Lake overlook loop</h2>
<p>A moderate half-day route with a wide view of the water.</p>
<a href="/routes/lake-overlook">View route</a>
</div>
</article>
</aside>
</article>
</main>.article-page {
width: min(100% - 2rem, 72rem);
margin-inline: auto;
}
h1 {
font-size: clamp(2rem, 1.25rem + 2.5vw, 3.75rem);
}
.feature-region {
container: feature / inline-size;
margin-block: 2rem;
}
.route-card {
display: grid;
grid-template-columns: minmax(0, 1fr);
overflow: clip;
border: 1px solid #d7dce5;
border-radius: 0.75rem;
}
.route-card img {
display: block;
width: 100%;
height: auto;
}
.route-card__body {
padding: 1rem;
}
.route-card__body a {
min-height: 2.75rem;
display: inline-flex;
align-items: center;
}
@container feature (width >= 42rem) {
.route-card {
grid-template-columns: minmax(14rem, 0.9fr) minmax(0, 1.1fr);
align-items: center;
}
}The page stays within a readable maximum width. The heading scales fluidly but has limits. The feature card queries its own allocated width rather than assuming a particular screen size. The image source changes crop for a narrow viewport and format when supported; the <img> supplies the fallback and alternative text. The link receives a comfortable minimum height.
For production, generate actual image variants with accurate intrinsic dimensions, make the media conditions match the intended crop transition, and add sizes where candidate width selection is used. Set image and card aspect ratios deliberately if avoiding layout shifts is important for a particular design.
Debugging on Mobile and Small Viewports
Desktop DevTools can emulate a viewport, but a physical device remains important for checking touch, browser chrome, font rendering, network behavior, and real input. Use both.
A systematic workflow
Reproduce the failure. Record the viewport width, orientation, zoom, route, and the content or interaction that overflows or disappears.
Use responsive device emulation. Drag the viewport continuously through the failure point rather than testing only preset device sizes. Check portrait and landscape where relevant.
Inspect the viewport configuration. Confirm the page declares
width=device-widthand has not disabled zoom.Find the overflowing element. In the console, compare
document.documentElement.scrollWidthwithdocument.documentElement.clientWidth, then inspect candidate elements' bounding boxes. A temporary outline can reveal which region crosses the viewport:* { outline: 1px solid rgb(255 0 0 / 15%); }Remove the temporary rule when finished. The document's
scrollWidthmay legitimately include a scrollable region, so identify the exact element before changing layout.Check intrinsic sizing. Long URLs, code, tables, images, and flex/grid minimum sizes commonly cause overflow. Add wrapping,
min-width: 0, responsive media sizing, or a local scrolling container according to the content's needs.Inspect the chosen image. In DevTools' Network panel, check the loaded source, resource dimensions, file size, and whether
sizesmatches the rendered slot. A blurry or oversized image often points to incorrect candidate metadata.Test interaction, not only appearance. Tap targets, keyboard focus, menus, dialogs, form errors, and hover-dependent controls all need testing.
Try enlarged text and zoom. Verify that content reflows and remains operable. WCAG reflow expectations generally require content to work at narrow equivalent widths without two-dimensional scrolling, except for content whose meaning or use inherently requires it, such as some data tables.
Validate on a real device. Confirm behavior with the actual browser controls, touch precision, network conditions, and orientation changes used by your audience.
Useful console checks
document.documentElement.clientWidth;
document.documentElement.scrollWidth;If scrollWidth is larger, use the element picker or a targeted query to find elements whose bounding rectangles exceed the viewport. Avoid immediately hiding overflow on body: this can conceal broken content and make controls unreachable rather than fixing the layout.
Common Traps
Breakpoints copied from a device chart
A layout may fail between the chosen device presets. Resize continuously and place breakpoints where content needs a different relationship.
Desktop-first overrides become difficult to reason about
Repeatedly undoing desktop declarations for small screens produces a cascade that is hard to maintain. Start with essential narrow-screen behavior and add enhancements at content-driven minimum-width breakpoints.
A container query cannot find the expected container
The querying element needs an eligible ancestor with container-type or the container shorthand. Check that the container is an ancestor, has the right name, and uses the intended axis. A component cannot query itself.
clamp() makes type too small or too large
A viewport-only preferred value can ignore the user's root font preference or produce unreadable extremes. Use a rem-based bound, inspect computed sizes, and test zoom, user font settings, and line wrapping.
sizes does not match the CSS slot
The browser may select an unnecessarily large candidate if sizes claims the image occupies more space than it does. It may select a low-resolution candidate if the slot is understated. Keep the hint synchronized with the real layout and verify downloads in the Network panel.
<picture> sources are ordered incorrectly
The first source whose media condition and type are supported is selected. Put specific art-directed sources before general format sources, and keep the final <img> as a meaningful fallback.
An image shifts the content when it loads
Without intrinsic dimensions or an aspect ratio, the browser may not know how much space to reserve. Supply accurate width and height attributes or a CSS aspect-ratio appropriate to the selected image.
Controls work only with hover or precise tapping
Hover is not a reliable interaction on touch and may not be available to keyboard users. Keep actions visible or provide an equivalent focus/tap behavior and a sufficiently large target.
Accessibility & Best Practices
- Preserve all essential information and actions at every viewport size.
- Keep source and focus order logical when changing the visual layout.
- Declare the responsive viewport and never prevent user zoom.
- Choose breakpoints from content needs, and use container queries for reusable components that live in variable-width regions.
- Allow long content to wrap or scroll within its own region; do not clip focus indicators or hide overflow without understanding it.
- Use
clamp()with readable minimum and maximum values and test text zoom. - Provide meaningful alternatives for informative images and empty alt text for decorative images.
- Supply intrinsic image dimensions or aspect ratios to reserve space.
- Keep
srcset,sizes, and actual CSS slot widths consistent; use<picture>when crops or formats vary. - Provide comfortable touch targets and spacing while keeping controls usable by keyboard and pointer.
- Test real devices, keyboard navigation, larger text, orientation changes, and intermediate widths.
Practice Exercises
Exercise 1 — Responsive course layout
A course page contains a title, a chapter navigation panel, and a reading article. On narrow screens, navigation should appear before the article in the reading order. Once the viewport is wide enough, the navigation should sit in a 16rem column beside the article. The article can contain long code lines.
Write mobile-first CSS for the page layout. Add a breakpoint based on available space, constrain the overall reading width, and ensure long code lines do not widen the page. Explain why the breakpoint should be chosen by resizing the actual content rather than copying a device width.
Exercise 2 — Responsive campaign image and heading
A campaign page has a square crop on narrow viewports and a wide crop on larger screens. Each crop is available as AVIF, WebP, and JPEG. The wide image is displayed fluidly in a centered content column. The heading should grow fluidly but never be smaller than 2rem or larger than 4rem.
Write the heading CSS and a <picture> element that selects the correct crop and format, with a JPEG fallback and useful alternative text. Explain how you would prevent image-induced layout shift and how srcset/sizes would change if multiple resolution variants were available for each crop.
Solutions
Solution 1
.course-page {
width: min(100% - 2rem, 76rem);
margin-inline: auto;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 1.5rem;
}
.course-page__article {
min-width: 0;
}
.course-page__article pre {
max-width: 100%;
overflow-x: auto;
}
@media (width >= 60rem) {
.course-page {
grid-template-columns: 16rem minmax(0, 1fr);
gap: 2rem;
}
}The DOM should place the navigation before the article. At the narrow size, the one-column grid retains that order. At wide sizes, the navigation takes a fixed track and the article receives the remaining space. minmax(0, 1fr) and min-width: 0 allow the article to shrink; the code block scrolls internally.
The 60rem value is an example, not a universal breakpoint. Resize with real titles, navigation labels, and article content to identify the point where two columns fit comfortably. A device preset cannot predict a user's zoom, browser font settings, split-screen width, or content length.
Solution 2
.campaign-title {
font-size: clamp(2rem, 1.2rem + 3vw, 4rem);
}
.campaign-image {
width: min(100% - 2rem, 70rem);
margin-inline: auto;
}
.campaign-image img {
display: block;
width: 100%;
height: auto;
}<picture>
<source media="(width < 42rem)" type="image/avif" srcset="/images/campaign-square.avif">
<source media="(width < 42rem)" type="image/webp" srcset="/images/campaign-square.webp">
<source media="(width < 42rem)" type="image/jpeg" srcset="/images/campaign-square.jpg">
<source type="image/avif" srcset="/images/campaign-wide.avif">
<source type="image/webp" srcset="/images/campaign-wide.webp">
<img
src="/images/campaign-wide.jpg"
width="1600"
height="900"
alt="A family exploring a forest trail at sunrise"
>
</picture>The clamp() bounds the fluid preferred size at 2rem and 4rem. The source order offers the narrow square crop in AVIF, WebP, then JPEG, followed by the wide crop in those formats; the <img> is the final fallback and owns the alternative text.
Provide accurate intrinsic width and height values for the selected image where possible, or reserve an appropriate aspect ratio in CSS so the browser can allocate space before download. The square crop has a different ratio than the wide crop, so use source-aware sizing or another technique if preserving exact geometry before each art-directed source loads is necessary.
If each crop has several resolution variants, use a separate width-descriptor srcset on each matching <source> and give it a sizes value that reflects the rendered image slot. Keep candidates within one source set on the same crop. The browser can then choose both the crop via the media condition and an appropriate resolution within that crop.