# Responsive Design

> 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).

- Skill: `jaykim88/responsive-design` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jaykim88/responsive-design`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jaykim88/responsive-design/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: JayKim88 (https://skillmd.com/u/jaykim88)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jaykim88/responsive-design

---


# 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

1. **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

2. **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

3. **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`)

4. **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

5. **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

6. **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`)

7. **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

8. **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**

```css
/* ❌ 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
- [ ] No horizontal scroll at 320 / 375 / 768 / 1024 / 1440px (long content uses `min-width:0` / `overflow-x` / `break-words`)
- [ ] Mobile-first (`min-width` queries, no desktop-first overrides)
- [ ] Breakpoints standardized as tokens, placed by content
- [ ] Full-height uses `dvh`/`svh`, not `100vh` (no mobile cutoff)
- [ ] Touch targets ≥ 44×44px with adequate spacing; hover-only affordances have a tap equivalent
- [ ] Zoom to 200% works (no `user-scalable=no`)
- [ ] Responsive images via `srcset` / `sizes`; no CLS from images

## 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.

