Responsive Design
Breakpoints, container queries, and mobile-first patterns in Tailwind CSS v4.
Search across all documentation pages
Breakpoints, container queries, and mobile-first patterns in Tailwind CSS v4.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
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:
lggrid-cols-1 to grid-cols-4)max-lg:hidden for hiding elements below a breakpointsticky sidebar on desktop@media (min-width: ...) - md:flex means @media (width >= 48rem) { display: flex }max-* variants compile to @media (width < ...) - useful for mobile-only styles@container on the parent and @sm:, @md:, etc. on children@3xs(12rem) @2xs(16rem) @xs(20rem) @sm(24rem) @md(28rem) @lg(32rem) @xl(36rem) @2xl(42rem) etc.rem by default, not pxCustom 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>
);
}// 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>
);
}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:hidden means hidden when viewport is strictly below md. At exactly 48rem, md: takes over.
Container queries need @container parent - Without it, @sm:flex does nothing. Fix: Always add @container to the element whose width you want to query.
hidden at mobile + breakpoint show - hidden md:flex works, but hidden md:block shows as block. 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.
| 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 |
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.
sm = 40rem, md = 48rem, lg = 64rem, xl = 80rem, 2xl = 96remrem, not px.<div className="hidden md:block xl:hidden">Tablet only</div>max-lg:hidden - hidden below lg (viewport < 64rem)lg:hidden - hidden at lg and above (viewport >= 64rem)@container to a parent element@sm:, @md:, @lg: etc. on children@theme {
--breakpoint-xs: 30rem;
--breakpoint-3xl: 120rem;
}<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.
Tailwind's scanner cannot detect dynamic class names built with template literals. Use complete static class names or a lookup map instead.
The parent element needs className="@container". Without it, there is no container to query against.
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.
Use the print: variant: print:hidden, print:text-black. This compiles to @media print.
<div className="@container/sidebar">
<div className="@md/sidebar:block">Named containers let you query a specific ancestor when multiple @container elements are nested.
Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥