Tailwind Responsive
Utility-first responsive design with sm:/md:/lg: prefixes, container queries plugin, and custom screens. You'll hit this when Tailwind's sm: prefix doesn't behave the way you expected at 640px.
<div className="flex-row md:flex-row sm:flex-col">
<Card />
<Card />
</div><div className="flex-row md:flex-row sm:flex-col">
<Card />
<Card />
</div><div className="flex flex-col sm:flex-row">
<Card />
<Card />
</div><div className="flex flex-col sm:flex-row">
<Card />
<Card />
</div>Two problems: flex-row and flex-col only set flex-direction and they don't add display: flex. Without the flex class, the container isn't a flex container at *any* screen size. Second, the responsive prefixes are backwards. Start with the mobile layout unprefixed, then override for larger screens.
Tailwind is mobile-first: unprefixed utilities apply to all screens, sm: applies at 640px+. Start with the mobile layout (flex-col), then override for larger screens (sm:flex-row). The base class is the smallest screen.
function Sidebar() {
const [show, setShow] = useState(
window.innerWidth >= 768,
);
// ...resize listener...
if (!show) return null;
return <nav>...</nav>;
}function Sidebar() {
const [show, setShow] = useState(
window.innerWidth >= 768,
);
// ...resize listener...
if (!show) return null;
return <nav>...</nav>;
}<nav className="hidden md:block">
{/* Sidebar content */}
</nav><nav className="hidden md:block">
{/* Sidebar content */}
</nav>window.innerWidth breaks SSR. A resize listener adds complexity for something CSS handles natively. The component unmounts on mobile, losing its state. Tailwind's hidden md:block is simpler and more robust.
hidden md:block compiles to display: none by default and display: block at 768px+. Pure CSS, no JavaScript, no hydration issues, no resize listeners. Tailwind's responsive prefixes are the standard way to show/hide elements.
<div className="grid grid-cols-4 gap-4">
{items.map((item) => (
<ItemCard key={item.id} item={item} />
))}
</div><div className="grid grid-cols-4 gap-4">
{items.map((item) => (
<ItemCard key={item.id} item={item} />
))}
</div><div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{items.map((item) => (
<ItemCard key={item.id} item={item} />
))}
</div><div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{items.map((item) => (
<ItemCard key={item.id} item={item} />
))}
</div>4 columns on a 320px phone means each card is ~72px wide (320px - gaps / 4). That's completely unusable. Always start with grid-cols-1 and add columns as the viewport grows.
Progressive column counts: 1 on mobile, 2 on tablet, 3 on small desktop, 4 on wide screens. Each breakpoint adds a column when there's enough space. Items are always readable and properly sized for the screen.
// Card responds to viewport, not parent
<div className="w-full md:w-1/3">
<div className="flex flex-col md:flex-row gap-4">
<img className="w-full md:w-1/3" />
<div>...</div>
</div>
</div>// Card responds to viewport, not parent
<div className="w-full md:w-1/3">
<div className="flex flex-col md:flex-row gap-4">
<img className="w-full md:w-1/3" />
<div>...</div>
</div>
</div>// Card responds to its container width
<div className="@container w-full md:w-1/3">
<div className="flex flex-col @sm:flex-row gap-4">
<img className="w-full @sm:w-1/3" />
<div>...</div>
</div>
</div>// Card responds to its container width
<div className="@container w-full md:w-1/3">
<div className="flex flex-col @sm:flex-row gap-4">
<img className="w-full @sm:w-1/3" />
<div>...</div>
</div>
</div>md:flex-row triggers at 768px viewport width regardless of where the card is placed. In a 300px sidebar on a 1440px screen, the card still switches to row layout because the *viewport* is wide. Container queries fix this.
Tailwind's @container and @sm: prefixes generate CSS container queries. The card adapts to its parent's width, not the viewport. If this card is in a sidebar, it stays in column layout even on a wide desktop.
<h1 className="text-4xl">
Welcome
</h1>
{/* 4xl is too large on mobile */}<h1 className="text-4xl">
Welcome
</h1>
{/* 4xl is too large on mobile */}<h1 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl">
Welcome
</h1><h1 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl">
Welcome
</h1>text-4xl (2.25rem / 36px) as a heading is fine on desktop but overwhelmingly large on a 320px phone where it might take 3 lines. Always start with a mobile-appropriate size and scale up.
Progressive font sizing from text-2xl (1.5rem) on mobile up to text-5xl (3rem) on large screens. Each breakpoint increases the size when there's room for it. This is the standard Tailwind pattern for responsive headings.