Responsive design — mobile-first, breakpoints, and container queries
Mobile-first: write base styles for narrow viewports, layer min-width media queries to upgrade. Use rem/em + clamp() for fluid type. Container queries (@container) let components respond to their slot, not the viewport.
Responsive design isn't "make it look fine on phone" — it's a discipline of designing for an unknown viewport, then layering breakpoints. The senior signal is mobile-first thinking, fluid units, and knowing container queries replaced 80% of media-query gymnastics.
Mobile-first means base styles target the narrowest viewport. You then layer min-width media queries to upgrade for larger screens. Why mobile-first?
- Mobile networks/devices are slower → smaller default CSS.
- Mobile layouts are usually single-column — the simpler, default flow.
- Larger-screen styles override only what's different.
- The mental model matches how you build: start simple, add complexity as space allows.
/* Base — phones */
.card { padding: 12px; font-size: 14px; }
/* Tablet+ */
@media (min-width: 640px) { .card { padding: 16px; font-size: 16px; } }
/* Desktop+ */
@media (min-width: 1024px) { .card { padding: 24px; max-width: 800px; } }Avoid max-width queries unless you genuinely need "this style only on phone" — they cascade backwards and pile up.
Standard breakpoints (Tailwind's, which most teams adopt):
sm: 640px (large phones / small tablets)md: 768px (tablets)lg: 1024px (laptops)xl: 1280px2xl: 1536px
Don't invent your own without reason. Pick a system; consumers and design tools assume them.
Fluid units beat hard breakpoints.
rem— sizes scale with the user's root font-size (a11y win).%,vw,vh,dvh— relative to viewport. Note: use100dvh(dynamic viewport height) instead of100vhto handle iOS Safari's address bar correctly.clamp(min, preferred, max)— fluid sizing that auto-clamps at extremes:
font-size: clamp(1rem, 0.5rem + 1.5vw, 1.5rem);
/* Scales with viewport but never below 16px or above 24px. */This is how modern design systems (Tailwind v4, design tokens like Radix/shadcn) avoid stair-stepping at every breakpoint.
Container queries (@container) — game-changer.
Media queries respond to the viewport. Container queries respond to a parent element's size. A card placed in a wide sidebar can render differently than the same card on a narrow page — without changing the card's component code.
.card-grid {
container-type: inline-size;
}
@container (min-width: 600px) {
.card { display: grid; grid-template-columns: 1fr 2fr; }
}Browser support is solid (Safari 16+, Chrome 105+). Use them whenever the question is "how should this component look at this width" — which is almost always.
Common patterns.
- Hidden on small vs hidden on large —
hidden md:blockpattern. - Stack → row —
flex-col md:flex-row. - Hamburger — show on mobile, full nav on desktop. Use
<button aria-expanded>for the hamburger. - Touch-first hit targets — 44px minimum tap area; padded buttons, larger nav links.
Images. Already covered separately — srcset/sizes for fluid responsive images, <picture> for art direction (different crops per viewport).
Testing.
- Real device > Chrome DevTools "responsive" mode. Especially iOS Safari.
- Browserslist + visual regression (Playwright/Chromatic) at multiple viewport widths.
prefers-reduced-motion,prefers-color-scheme,prefers-contrastare responsive-design concerns too — design tokens swap on these.
Common mistakes.
- Desktop-first with
max-widthqueries piling up — overrides cascade poorly. - Hard-coded breakpoint values everywhere instead of design tokens.
- Using
pxfor font-size and ignoring user's root font-size preference (a11y). 100vhcontainers on iOS Safari — leaves a gap when the address bar collapses; use100dvh.- Hiding content with
display: noneon mobile but expecting it to count for SEO/a11y — it doesn't.
Code
Follow-up questions
- •Why mobile-first instead of desktop-first?
- •When do you reach for container queries over media queries?
- •What's the difference between vh and dvh?
- •How does clamp() avoid stair-stepping at breakpoints?
Common mistakes
- •Desktop-first → max-width queries everywhere → cascade chaos.
- •Using vh for full-screen layouts on iOS Safari — leaves gaps.
- •Hard-coded px for font-size — ignores user preference.
- •Putting media queries inline per component instead of design tokens.
Performance considerations
- •Use container-type: inline-size only — full size triggers more layout work.
- •clamp() is computed on every layout — fine, but don't go nuts on critical-path elements.
Edge cases
- •Container queries don't work on display: contents elements.
- •100dvh vs 100svh vs 100lvh — small/dynamic/large viewport variants for mobile.
- •Print media (@media print) — separate ruleset entirely.
Real-world examples
- •Tailwind responsive utilities, shadcn/ui card components using container queries, design systems with clamp-based typography (Stripe, Linear).