Responsive Design
Purpose
Layouts adapt from 320px phones to wide desktops without horizontal scroll, overlap, or unreadable text. Mobile-first: base styles target the smallest screen; breakpoints add complexity upward, never strip it down.
Universal — mobile-first ordering, breakpoint-as-content-decision, fluid type/space, container queries, and touch-target minimums are CSS / web-platform concerns identical across frameworks.
Procedure
Start mobile-first
- Base styles target the narrowest viewport (≥ 320px); add complexity at larger breakpoints with
min-width queries
- Never desktop-first (
max-width overrides) — it leaks desktop assumptions into mobile and is harder to reason about
Set breakpoints by content, not by device
- Add a breakpoint where the layout breaks, not at fixed "phone / tablet / desktop" widths
- Standardize the set as design tokens (coordinate with
design-system-construction); don't scatter arbitrary @media (768px) values
Prefer intrinsic layout over breakpoints where possible
- Flexbox
wrap + min()/max()/clamp() and Grid auto-fit / minmax adapt without explicit breakpoints
- Fewer breakpoints = fewer states to test
- Use logical properties (
margin-inline, inset-block) over left/right so layouts also work in RTL (coordinate with i18n-localization)
Use container queries for reusable components
- A card in a sidebar vs. main column should respond to its container, not the viewport
- Viewport media queries are the wrong tool for components reused at different container sizes
Fluid type and spacing
clamp(min, preferred-vw, max) for headings / section spacing — smooth scaling instead of stepped jumps
- Cap line length ~60-75ch for readability
Responsive images
srcset / sizes (or the framework's image component) so phones don't download desktop-sized images
<picture> for art direction (different crop/aspect on mobile vs desktop) and modern formats (AVIF/WebP with fallback); a wrong sizes value silently defeats srcset
- Explicit dimensions /
aspect-ratio to prevent CLS (coordinate with rendering-performance)
Touch targets and zoom
- Interactive targets ≥ 44×44px (Apple HIG) / 48×48px (Material); adequate spacing between adjacent targets
- Never
user-scalable=no / maximum-scale=1 — blocks pinch-zoom (WCAG 1.4.4 failure)
<meta name="viewport" content="width=device-width, initial-scale=1"> present
- Interaction, not just size: hover-only affordances (tooltips, hover menus) don't exist on touch — gate them with
@media (hover: hover) / pointer: coarse and give a tap equivalent
- Form inputs ≥ 16px font-size on iOS, or focusing one auto-zooms the page (jarring)
7b. Guard against overflow and mobile viewport quirks
min-width: 0 on flex/grid children — the #1 hidden cause of horizontal scroll: flex items default to min-width: auto and refuse to shrink below their content (a long string or <pre> blows out the layout). Set min-width: 0 (min-w-0) on the shrinking child
- Long content: wrap wide tables in
overflow-x: auto; overflow-wrap: anywhere / break-words for long URLs and code
- Mobile viewport units:
100vh is wrong on mobile — browser chrome makes vh taller than the visible area, so content is cut off or jumps when the bar hides. Use dvh / svh / lvh (dynamic / small / large viewport)
- Safe areas: for full-bleed or fixed-bottom UI on notched phones, set
viewport-fit=cover + env(safe-area-inset-*) padding
- Test across viewports (validation loop)
- Walk 320 / 375 / 768 / 1024 / 1440px; for each: no horizontal scroll, no overlap, text readable, targets tappable
- Fix each break and re-walk until all viewports are clean
- Also test landscape orientation + browser zoom to 200%
- Verify on at least one real device — DevTools device mode doesn't reproduce real touch, browser chrome (the
100vh issue), or performance
Before / After
Desktop-first override vs. mobile-first base
/* ❌ Desktop-first — mobile is an afterthought, overrides pile up */
.grid { display: grid; grid-template-columns: repeat(3, 1fr); }
@media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }
/* ✅ Mobile-first — simplest case is the base, complexity added upward */
.grid { display: grid; grid-template-columns: 1fr; }
@media (min-width: 768px) { .grid { grid-template-columns: repeat(3, 1fr); } }
/* ✅ Better — intrinsic, no breakpoint needed */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); }
Anti-patterns
| ❌ Anti-pattern |
✅ Correct |
Desktop-first max-width overrides |
Mobile-first min-width, complexity added upward |
@media (768px) scattered with arbitrary values |
Breakpoint tokens placed where the layout actually breaks |
| Viewport media query inside a reused component |
Container query (@container) responding to its parent |
user-scalable=no to "fix" layout |
Allow zoom; fix the layout instead (WCAG 1.4.4) |
Stepped font-size per breakpoint |
clamp() for fluid scaling |
| One image size for all devices |
srcset / sizes or framework image component |
height: 100vh on mobile (content cut off) |
100dvh / svh (accounts for browser chrome) |
| Flex child overflowing on long content |
min-width: 0 on the shrinking child |
| Hover-only menu / tooltip on touch |
@media (hover: hover) + a tap equivalent |
Severity tiers
| Tier |
Examples |
Action SLA |
| Critical |
Horizontal scroll, content overlap, or unreadable text at a common viewport (320 / 375 / 768px); content cut off by 100vh on mobile (use dvh); zoom blocked via user-scalable=no / maximum-scale=1 (WCAG 1.4.4 failure) |
Block release; fix immediately |
| Major |
Touch targets < 44×44px or too closely spaced; hover-only affordance unusable on touch; desktop-first max-width overrides; oversized images served to mobile (no srcset) causing slow load or CLS |
Fix this sprint |
| Minor |
Stepped font-size per breakpoint instead of clamp(); arbitrary scattered @media values not tokenized; line length exceeding ~75ch; missing safe-area padding on notched devices |
Schedule within 2 sprints |
Completion Criteria
Output
- Responsive styles: mobile-first, breakpoint tokens,
clamp() fluid scales, container queries for reusable components
- Viewport test report (paste into PR): per-breakpoint pass/fail (320 / 375 / 768 / 1024 / 1440), landscape, 200% zoom
- Commit format:
fix(responsive): <component> at <breakpoint> / feat(responsive): mobile-first <layout>
Implementation
React + Next.js (default)
- Breakpoints: Tailwind
sm/md/lg/xl/2xl (mobile-first by default — unprefixed = base, md: = ≥ 768px)
- Fluid: Tailwind arbitrary
clamp() or theme.fontSize tokens; container queries via @tailwindcss/container-queries (@container, @md:)
- Images:
next/image with sizes prop (e.g., sizes="(max-width: 768px) 100vw, 50vw"); <picture> for art direction
- Touch targets: enforce a min interactive size in the design-system components
- Viewport meta: in root
layout.tsx (Next.js sets a sensible default — verify no user-scalable=no)
- Viewport units / overflow: Tailwind
h-dvh / min-h-dvh, min-w-0 on flex children, break-words / overflow-x-auto for long content
- Safe area:
env(safe-area-inset-*) (arbitrary pt-[env(safe-area-inset-top)] or a plugin) with viewport-fit=cover
- Hover capability: gate hover affordances with
[@media(hover:hover)]: so they don't strand touch users
Other stacks
- Vue / Nuxt: Tailwind identical;
<NuxtImg sizes> for responsive images; useMediaQuery (VueUse) for JS-side breakpoint logic
- SvelteKit: Tailwind identical;
@sveltejs/enhanced-img for responsive images; CSS container queries native
- Angular: Tailwind or Angular CDK
BreakpointObserver; NgOptimizedImage with sizes
- Universal: mobile-first
min-width, CSS container queries (@container), clamp(), srcset / sizes, and the 44px touch-target minimum are all web-platform standards — framework-independent
Related skills
design-system-construction — breakpoints + fluid scales belong in design tokens
rendering-performance — responsive images affect LCP / CLS
accessibility-audit — touch-target size + zoom are WCAG requirements
i18n-localization — logical properties + RTL layout mirroring
Reference
- Key insight encoded: Add breakpoints where the content breaks, not at device widths, and prefer intrinsic layout (
clamp, Grid auto-fit, container queries) so there are fewer explicit states to maintain and test. Mobile-first min-width ordering keeps the simplest styles as the base and adds complexity upward. The most common real-world mobile bugs aren't breakpoints: 100vh cuts off content (use dvh), flex children overflow without min-width: 0, and hover-only UI strands touch users — guard all three.
1---2name: responsive-design3description: Build mobile-first responsive layouts — breakpoint strategy, fluid type/space with clamp(), container queries, mobile viewport units (dvh) + overflow robustness, touch-target testing, responsive images. Use when adding a layout, when mobile bugs appear, or before shipping a public page. Not for codifying breakpoint/fluid scales as design tokens (use design-system-construction) or auditing touch-target size and zoom against WCAG (use accessibility-audit).4license: MIT5---67# Responsive Design89## Purpose10Layouts adapt from 320px phones to wide desktops without horizontal scroll, overlap, or unreadable text. Mobile-first: base styles target the smallest screen; breakpoints add complexity upward, never strip it down.1112**Universal** — mobile-first ordering, breakpoint-as-content-decision, fluid type/space, container queries, and touch-target minimums are CSS / web-platform concerns identical across frameworks.1314## Procedure15161. **Start mobile-first**17 - Base styles target the narrowest viewport (≥ 320px); add complexity at larger breakpoints with `min-width` queries18 - Never desktop-first (`max-width` overrides) — it leaks desktop assumptions into mobile and is harder to reason about19202. **Set breakpoints by content, not by device**21 - Add a breakpoint where the layout *breaks*, not at fixed "phone / tablet / desktop" widths22 - Standardize the set as design tokens (coordinate with `design-system-construction`); don't scatter arbitrary `@media (768px)` values23243. **Prefer intrinsic layout over breakpoints where possible**25 - Flexbox `wrap` + `min()/max()/clamp()` and Grid `auto-fit` / `minmax` adapt without explicit breakpoints26 - Fewer breakpoints = fewer states to test27 - Use logical properties (`margin-inline`, `inset-block`) over `left`/`right` so layouts also work in RTL (coordinate with `i18n-localization`)28294. **Use container queries for reusable components**30 - A card in a sidebar vs. main column should respond to *its container*, not the viewport31 - Viewport media queries are the wrong tool for components reused at different container sizes32335. **Fluid type and spacing**34 - `clamp(min, preferred-vw, max)` for headings / section spacing — smooth scaling instead of stepped jumps35 - Cap line length ~60-75ch for readability36376. **Responsive images**38 - `srcset` / `sizes` (or the framework's image component) so phones don't download desktop-sized images39 - `<picture>` for art direction (different crop/aspect on mobile vs desktop) and modern formats (AVIF/WebP with fallback); a wrong `sizes` value silently defeats `srcset`40 - Explicit dimensions / `aspect-ratio` to prevent CLS (coordinate with `rendering-performance`)41427. **Touch targets and zoom**43 - Interactive targets ≥ 44×44px (Apple HIG) / 48×48px (Material); adequate spacing between adjacent targets44 - Never `user-scalable=no` / `maximum-scale=1` — blocks pinch-zoom (WCAG 1.4.4 failure)45 - `<meta name="viewport" content="width=device-width, initial-scale=1">` present46 - Interaction, not just size: hover-only affordances (tooltips, hover menus) don't exist on touch — gate them with `@media (hover: hover)` / `pointer: coarse` and give a tap equivalent47 - Form inputs ≥ 16px font-size on iOS, or focusing one auto-zooms the page (jarring)48497b. **Guard against overflow and mobile viewport quirks**50 - **`min-width: 0` on flex/grid children** — the #1 hidden cause of horizontal scroll: flex items default to `min-width: auto` and refuse to shrink below their content (a long string or `<pre>` blows out the layout). Set `min-width: 0` (`min-w-0`) on the shrinking child51 - Long content: wrap wide tables in `overflow-x: auto`; `overflow-wrap: anywhere` / `break-words` for long URLs and code52 - **Mobile viewport units**: `100vh` is wrong on mobile — browser chrome makes `vh` taller than the visible area, so content is cut off or jumps when the bar hides. Use `dvh` / `svh` / `lvh` (dynamic / small / large viewport)53 - **Safe areas**: for full-bleed or fixed-bottom UI on notched phones, set `viewport-fit=cover` + `env(safe-area-inset-*)` padding54558. **Test across viewports (validation loop)**56 - Walk 320 / 375 / 768 / 1024 / 1440px; for each: no horizontal scroll, no overlap, text readable, targets tappable57 - Fix each break and re-walk until all viewports are clean58 - Also test landscape orientation + browser zoom to 200%59 - Verify on at least one real device — DevTools device mode doesn't reproduce real touch, browser chrome (the `100vh` issue), or performance6061## Before / After6263**Desktop-first override vs. mobile-first base**6465```css66/* ❌ Desktop-first — mobile is an afterthought, overrides pile up */67.grid { display: grid; grid-template-columns: repeat(3, 1fr); }68@media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }6970/* ✅ Mobile-first — simplest case is the base, complexity added upward */71.grid { display: grid; grid-template-columns: 1fr; }72@media (min-width: 768px) { .grid { grid-template-columns: repeat(3, 1fr); } }7374/* ✅ Better — intrinsic, no breakpoint needed */75.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); }76```7778## Anti-patterns7980| ❌ Anti-pattern | ✅ Correct |81|---|---|82| Desktop-first `max-width` overrides | Mobile-first `min-width`, complexity added upward |83| `@media (768px)` scattered with arbitrary values | Breakpoint tokens placed where the layout actually breaks |84| Viewport media query inside a reused component | Container query (`@container`) responding to its parent |85| `user-scalable=no` to "fix" layout | Allow zoom; fix the layout instead (WCAG 1.4.4) |86| Stepped `font-size` per breakpoint | `clamp()` for fluid scaling |87| One image size for all devices | `srcset` / `sizes` or framework image component |88| `height: 100vh` on mobile (content cut off) | `100dvh` / `svh` (accounts for browser chrome) |89| Flex child overflowing on long content | `min-width: 0` on the shrinking child |90| Hover-only menu / tooltip on touch | `@media (hover: hover)` + a tap equivalent |9192## Severity tiers9394| Tier | Examples | Action SLA |95|---|---|---|96| **Critical** | Horizontal scroll, content overlap, or unreadable text at a common viewport (320 / 375 / 768px); content cut off by `100vh` on mobile (use `dvh`); zoom blocked via `user-scalable=no` / `maximum-scale=1` (WCAG 1.4.4 failure) | Block release; fix immediately |97| **Major** | Touch targets < 44×44px or too closely spaced; hover-only affordance unusable on touch; desktop-first `max-width` overrides; oversized images served to mobile (no `srcset`) causing slow load or CLS | Fix this sprint |98| **Minor** | Stepped `font-size` per breakpoint instead of `clamp()`; arbitrary scattered `@media` values not tokenized; line length exceeding ~75ch; missing safe-area padding on notched devices | Schedule within 2 sprints |99100## Completion Criteria101- [ ] No horizontal scroll at 320 / 375 / 768 / 1024 / 1440px (long content uses `min-width:0` / `overflow-x` / `break-words`)102- [ ] Mobile-first (`min-width` queries, no desktop-first overrides)103- [ ] Breakpoints standardized as tokens, placed by content104- [ ] Full-height uses `dvh`/`svh`, not `100vh` (no mobile cutoff)105- [ ] Touch targets ≥ 44×44px with adequate spacing; hover-only affordances have a tap equivalent106- [ ] Zoom to 200% works (no `user-scalable=no`)107- [ ] Responsive images via `srcset` / `sizes`; no CLS from images108109## Output110- **Responsive styles**: mobile-first, breakpoint tokens, `clamp()` fluid scales, container queries for reusable components111- **Viewport test report** (paste into PR): per-breakpoint pass/fail (320 / 375 / 768 / 1024 / 1440), landscape, 200% zoom112- **Commit format**: `fix(responsive): <component> at <breakpoint>` / `feat(responsive): mobile-first <layout>`113114## Implementation115116### React + Next.js (default)117- Breakpoints: Tailwind `sm/md/lg/xl/2xl` (mobile-first by default — unprefixed = base, `md:` = ≥ 768px)118- Fluid: Tailwind arbitrary `clamp()` or `theme.fontSize` tokens; container queries via `@tailwindcss/container-queries` (`@container`, `@md:`)119- Images: `next/image` with `sizes` prop (e.g., `sizes="(max-width: 768px) 100vw, 50vw"`); `<picture>` for art direction120- Touch targets: enforce a min interactive size in the design-system components121- Viewport meta: in root `layout.tsx` (Next.js sets a sensible default — verify no `user-scalable=no`)122- Viewport units / overflow: Tailwind `h-dvh` / `min-h-dvh`, `min-w-0` on flex children, `break-words` / `overflow-x-auto` for long content123- Safe area: `env(safe-area-inset-*)` (arbitrary `pt-[env(safe-area-inset-top)]` or a plugin) with `viewport-fit=cover`124- Hover capability: gate hover affordances with `[@media(hover:hover)]:` so they don't strand touch users125126### Other stacks127- **Vue / Nuxt**: Tailwind identical; `<NuxtImg sizes>` for responsive images; `useMediaQuery` (VueUse) for JS-side breakpoint logic128- **SvelteKit**: Tailwind identical; `@sveltejs/enhanced-img` for responsive images; CSS container queries native129- **Angular**: Tailwind or Angular CDK `BreakpointObserver`; `NgOptimizedImage` with `sizes`130- **Universal**: mobile-first `min-width`, CSS container queries (`@container`), `clamp()`, `srcset` / `sizes`, and the 44px touch-target minimum are all web-platform standards — framework-independent131132## Related skills133- `design-system-construction` — breakpoints + fluid scales belong in design tokens134- `rendering-performance` — responsive images affect LCP / CLS135- `accessibility-audit` — touch-target size + zoom are WCAG requirements136- `i18n-localization` — logical properties + RTL layout mirroring137138## Reference139- **Key insight encoded**: Add breakpoints where the *content* breaks, not at device widths, and prefer intrinsic layout (`clamp`, Grid `auto-fit`, container queries) so there are fewer explicit states to maintain and test. Mobile-first `min-width` ordering keeps the simplest styles as the base and adds complexity upward. The most common real-world mobile bugs aren't breakpoints: `100vh` cuts off content (use `dvh`), flex children overflow without `min-width: 0`, and hover-only UI strands touch users — guard all three.