Responsive Design
Breakpoints, container queries, and mobile-first patterns in Tailwind CSS v4.
Recipe
Quick-reference recipe card — copy-paste ready.
// Mobile-first breakpoints (min-width)
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
// Default breakpoints: sm(40rem) md(48rem) lg(64rem) xl(80rem) 2xl(96rem)
// Max-width variant
<div className="max-lg:hidden"> {/* hidden below lg */}
// Range
<div className="md:max-xl:flex"> {/* flex only between md and xl */}
// Container queries
<div className="@container">
<div className="@sm:flex @lg:grid @lg:grid-cols-2">
{/* responds to container width, not viewport */}
</div>
</div>
// Named containers
<div className="@container/sidebar">
<div className="@md/sidebar:block">When to reach for this: When your layout or component needs to adapt across screen sizes or container sizes.
Working Example
export function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen flex-col lg:flex-row">
{/* Sidebar — full width on mobile, fixed width on desktop */}
<aside className="border-b bg-gray-50 p-4 lg:w-64 lg:shrink-0 lg:border-b-0 lg:border-r">
<nav className="flex gap-2 overflow-x-auto lg:flex-col lg:gap-1 lg:overflow-visible">
<a href="#" className="whitespace-nowrap rounded px-3 py-2 text-sm hover:bg-gray-200 lg:whitespace-normal">
Dashboard
</a>
<a href="#" className="whitespace-nowrap rounded px-3 py-2 text-sm hover:bg-gray-200 lg:whitespace-normal">
Analytics
</a>
<a href="#" className="whitespace-nowrap rounded px-3 py-2 text-sm hover:bg-gray-200 lg:whitespace-normal">
Settings
</a>
</nav>
</aside>
{/* Main content */}
<main className="flex-1 p-4 md:p-6 lg:p-8">
{/* Stats grid — responsive columns */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<StatCard label="Revenue" value="$12,345" />
<StatCard label="Users" value="1,234" />
<StatCard label="Orders" value="567" />
<StatCard label="Conversion" value="3.2%" />
</div>
{/* Content area with container queries */}
<div className="mt-6 grid gap-6 lg:grid-cols-[1fr_300px]">
<section className="@container">
<div className="@md:grid @md:grid-cols-2 @md:gap-4 space-y-4 @md:space-y-0">
{children}
</div>
</section>
<aside className="max-lg:hidden">
<div className="sticky top-4 rounded border p-4">
<h3 className="font-medium">Activity Feed</h3>
</div>
</aside>
</div>
</main>
</div>
);
}
function StatCard({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-lg border bg-white p-4 shadow-sm">
<p className="text-sm text-gray-500">{label}</p>
<p className="mt-1 text-2xl font-bold">{value}</p>
</div>
);
}What this demonstrates:
- Mobile-first layout that shifts from stacked to sidebar on
lg - Horizontal scroll nav on mobile, vertical on desktop
- Responsive grid columns (
grid-cols-1togrid-cols-4) - Container queries for content-area responsive layout
max-lg:hiddenfor hiding elements below a breakpointstickysidebar on desktop
Deep Dive
How It Works
- Tailwind uses a mobile-first approach: unprefixed utilities apply to all sizes, breakpoint prefixes apply at that size and up
- Breakpoints compile to
@media (min-width: ...)—md:flexmeans@media (width >= 48rem) { display: flex } max-*variants compile to@media (width < ...)— useful for mobile-only styles- Container queries use
@containeron the parent and@sm:,@md:, etc. on children - Container query breakpoints:
@3xs(12rem)@2xs(16rem)@xs(20rem)@sm(24rem)@md(28rem)@lg(32rem)@xl(36rem)@2xl(42rem)etc. - v4 breakpoints are defined in
remby default, notpx
Variations
Custom breakpoints:
@theme {
--breakpoint-xs: 30rem; /* 480px */
--breakpoint-3xl: 120rem; /* 1920px */
}Container query with custom sizes:
@theme {
--container-4xs: 8rem;
}Responsive typography:
<h1 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl">
Responsive Heading
</h1>
{/* Or use clamp for fluid sizing */}
<h1 className="text-[clamp(1.5rem,4vw,3rem)]">
Fluid Heading
</h1>Responsive show/hide:
{/* Show on mobile only */}
<div className="md:hidden">Mobile nav</div>
{/* Show on desktop only */}
<div className="hidden md:block">Desktop nav</div>
{/* Show between md and xl */}
<div className="hidden md:block xl:hidden">Tablet only</div>Container query card:
function AdaptiveCard({ children }: { children: React.ReactNode }) {
return (
<div className="@container">
<div className="flex flex-col @sm:flex-row @sm:items-center gap-4 rounded border p-4">
<div className="size-16 shrink-0 rounded bg-gray-200 @sm:size-20" />
<div className="flex-1">{children}</div>
</div>
</div>
);
}TypeScript Notes
// Responsive variants are class names — no TS impact
// But you can type breakpoint-dependent props:
interface LayoutProps {
columns?: {
default: number;
sm?: number;
md?: number;
lg?: number;
};
}
function Grid({ columns }: LayoutProps) {
const colClasses = {
1: "grid-cols-1",
2: "grid-cols-2",
3: "grid-cols-3",
4: "grid-cols-4",
} as const;
return (
<div className={cn(
"grid gap-4",
colClasses[columns.default as keyof typeof colClasses],
columns.sm && `sm:${colClasses[columns.sm as keyof typeof colClasses]}`,
)}>
{/* Note: dynamic class construction does not work with Tailwind — see Gotchas */}
</div>
);
}Gotchas
-
Dynamic class names are not detected —
sm:grid-cols-${n}is never found by Tailwind's scanner. Fix: Use complete static class names or safelist them. -
max-*is<, not<=—max-md:hiddenmeans hidden when viewport is strictly belowmd. At exactly48rem,md:takes over. -
Container queries need
@containerparent — Without it,@sm:flexdoes nothing. Fix: Always add@containerto the element whose width you want to query. -
hiddenat mobile + breakpoint show —hidden md:flexworks, buthidden md:blockshows asblock. Make sure the display value matches what you want. -
Print breakpoint — Use
print:for print-specific styles:print:hidden,print:text-black. Often forgotten but important for printable pages.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
CSS @media directly | You need complex media queries (hover, prefers-reduced-motion) | Standard breakpoints suffice |
CSS @container directly | You need container query features beyond Tailwind's support | Tailwind's @container utilities cover your needs |
useMediaQuery hook | You need JS-level responsive logic (different components) | CSS-only layout changes suffice |
Responsive images (srcset) | You need different image files per breakpoint | You only need layout changes |
FAQs
What does "mobile-first" mean in Tailwind's breakpoint system?
Unprefixed utilities apply to all screen sizes. Breakpoint prefixes like md: apply at that size and up (min-width). You style mobile first, then layer on larger-screen overrides.
What are the default breakpoint values in Tailwind v4?
sm= 40rem,md= 48rem,lg= 64rem,xl= 80rem,2xl= 96rem- They are defined in
rem, notpx.
How do you show an element only between md and xl?
<div className="hidden md:block xl:hidden">Tablet only</div>What is the difference between max-lg:hidden and lg:hidden?
max-lg:hidden— hidden belowlg(viewport < 64rem)lg:hidden— hidden atlgand above (viewport >= 64rem)
How do container queries work in Tailwind v4?
- Add
@containerto a parent element - Use
@sm:,@md:,@lg:etc. on children - Children respond to the container's width, not the viewport
How do you define a custom breakpoint in v4?
@theme {
--breakpoint-xs: 30rem;
--breakpoint-3xl: 120rem;
}What is the fluid typography approach with clamp()?
<h1 className="text-[clamp(1.5rem,4vw,3rem)]">Fluid Heading</h1>The font size scales smoothly between 1.5rem and 3rem based on viewport width.
Gotcha: You wrote sm:grid-cols-${n} but the class is not applied. Why?
Tailwind's scanner cannot detect dynamic class names built with template literals. Use complete static class names or a lookup map instead.
Gotcha: Container query classes like @sm:flex do nothing. What is missing?
The parent element needs className="@container". Without it, there is no container to query against.
How would you type breakpoint-dependent props in TypeScript?
interface LayoutProps {
columns?: {
default: number;
sm?: number;
md?: number;
lg?: number;
};
}Then use a static lookup map to convert numbers to class strings — never interpolate dynamically.
How do you apply styles only for print?
Use the print: variant: print:hidden, print:text-black. This compiles to @media print.
What is a named container and when would you use one?
<div className="@container/sidebar">
<div className="@md/sidebar:block">Named containers let you query a specific ancestor when multiple @container elements are nested.
Related
- Setup — configuring custom breakpoints
- Utilities — core utility classes
- Dark Mode — dark mode responsive patterns
- Custom Utilities — custom responsive utilities