Responsive design is CSS first. React gives you composition; CSS gives you layout. This guide shows how to combine mobile‑first utilities, fluid grids, container queries, and clamp‑based typography—with minimal JavaScript—to ship a system that handles small phones to large desktops gracefully.
You’ll get: Tailwind screen config, grid recipes, container query patterns, a hit‑target checklist, and a small hook for the few places you truly need JS media queries.
Table of Contents
- Mobile‑first mindset
- Tailwind screens & tokens
- Fluid grids with CSS Grid
- Container queries for components
- Fluid typography with
clamp() - Touch targets & inputs
- Conditional rendering without layout shift
- A tiny
useBreakpointhook (when needed) - Testing across viewports & zoom
- Reference snippets
- FAQ
Mobile‑first mindset
- Start styles at base; add enhancements at breakpoints (
sm,md,lg,xl). - Avoid JS for layout; reserve it for behavior, not positioning.
- Respect reduced motion and prefers-color-scheme with CSS.
Tailwind screens & tokens
// tailwind.config.ts (excerpt)
export default {
theme: {
screens: {
sm: "640px",
md: "768px",
lg: "1024px",
xl: "1280px",
"2xl": "1536px",
},
extend: {
spacing: {
18: "4.5rem",
},
},
},
};Use semantic spacing/typography tokens and map to CSS variables for theming.
Fluid grids with CSS Grid
Let the browser pack items intelligently:
// ResponsiveCardGrid.tsx
export function ResponsiveCardGrid({
children,
}: {
children: React.ReactNode;
}) {
return (
<div
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3
[--min:16rem] [--gap:1rem]"
style={{
gridTemplateColumns: "repeat(auto-fit, minmax(var(--min), 1fr))",
gap: "var(--gap)",
}}
>
{children}
</div>
);
}For equal height cards, use align-content: start and internal flex layouts.
Container queries for components
Use container queries so components adapt to their parent width, not just the viewport.
/* component.css */
.card {
container-type: inline-size;
}
@container (min-width: 420px) {
.card--media {
display: grid;
grid-template-columns: 180px 1fr;
gap: 1rem;
}
}// Card.tsx
export function Card({ children }: { children: React.ReactNode }) {
return <article className="card rounded-md border p-4">{children}</article>;
}Set
container-typeon the parent that defines the component’s layout boundary.
Fluid typography with clamp()
:root {
/* 320 → 1280 px viewport */
--fs-title: clamp(1.25rem, 0.9rem + 1.2vw, 2rem);
--fs-body: clamp(0.95rem, 0.8rem + 0.4vw, 1.125rem);
}
.h-title {
font-size: var(--fs-title);
line-height: 1.2;
}
.p-body {
font-size: var(--fs-body);
line-height: 1.55;
}Bind these to Tailwind via theme extensions if desired.
Touch targets & inputs
- Minimum 44×44px hit targets.
- Spacing between interactive controls to prevent accidental taps.
- Use
inputmode/typefor on‑screen keyboards (e.g.,type="email",inputmode="numeric"). - Keep focus styles visible; use
:focus-visiblenotoutline: none.
Conditional rendering without layout shift
Reserve space for items that appear at larger breakpoints to avoid CLS.
<div className="hidden md:block md:w-64">{/* sidebar content */}</div>Skeletons with fixed dimensions help maintain stability.
A tiny useBreakpoint hook (when needed)
CSS should do most of the work. For logic (e.g., switch chart type), use a matchMedia hook.
import { useEffect, useState } from "react";
export function useBreakpoint(query = "(min-width: 1024px)") {
const [match, setMatch] = useState<boolean | null>(null);
useEffect(() => {
const m = window.matchMedia(query);
const onChange = () => setMatch(m.matches);
onChange();
m.addEventListener("change", onChange);
return () => m.removeEventListener("change", onChange);
}, [query]);
return match;
}Testing across viewports & zoom
- Test with 200% zoom for reflow issues.
- Snapshot key screens at mobile/tablet/desktop with Playwright.
- Emulate reduced motion and dark mode to catch theme/animation bugs.
await page.setViewportSize({ width: 360, height: 800 });
await expect(page).toHaveScreenshot("home-mobile.png");Reference snippets
Aspect‑ratio media
<div className="aspect-[16/9] overflow-hidden rounded">
<img src="/thumb.jpg" alt="Preview" className="h-full w-full object-cover" />
</div>Responsive nav
<nav className="flex items-center justify-between gap-3">
<div className="text-lg font-semibold">Brand</div>
<button className="md:hidden rounded border px-3 py-2">Menu</button>
<ul className="hidden items-center gap-4 md:flex">
<li>
<a className="hover:underline" href="/features">
Features
</a>
</li>
<li>
<a className="hover:underline" href="/pricing">
Pricing
</a>
</li>
</ul>
</nav>FAQ
Should I use JS for breakpoints?
Only for behavioral switches. Layout should remain CSS‑driven with media/container queries.
Grid or flex for cards?
Use Grid for two‑dimensional layouts (rows/cols) and Flex for one‑dimensional flows.
Do I still need Bootstrap‑style containers?
Use a max‑width wrapper with padding, but let grids be fluid and content‑aware.
Next steps
- Define screens and fluid type tokens.
- Add container queries to your key components.
- Snapshot three breakpoints with Playwright to prevent regressions.

