Tailwind v4 Container Queries: A Practical Guide with Real Examples
Container queries landed as first-class citizens in Tailwind CSS v4. The short version: you can now style an element based on the size of its parent container, not just the viewport. That unlocks truly reusable components that adapt to wherever you drop them.
This guide shows the exact syntax, real-world patterns we use in every Craftly template, and the three gotchas that aren't in the docs.
What problem container queries solve
Before container queries, responsive design was anchored to viewport width. A card component on a full-width mobile layout and a card component tucked into a 300px sidebar on desktop would both get the same media-query breakpoints. The sidebar card looked broken.
Container queries fix this by letting the card respond to its own available width instead. Move it from full-width to sidebar and it adapts, same component.
The basic pattern in Tailwind v4
Three steps:
Step 1: Mark the container
Put the @container class on whatever parent should be the query target:
<div className="@container">
<Card />
</div>
Step 2: Use container variants on children
Inside Card, use @sm:, @md:, @lg: etc. as variant prefixes β they key off the container, not the viewport:
<div className="p-4 @sm:p-6 @lg:p-8">
<h3 className="text-base @md:text-lg @lg:text-xl">Card title</h3>
</div>
Now that card responds to its own parent's width. Drop it in a full-width grid cell, it renders at the @lg size. Drop it in a narrow sidebar, it stays at the base size.
Step 3: Container sizes default
The default breakpoints are:
| Variant | Container width |
|---|---|
@xs |
20rem (320px) |
@sm |
24rem (384px) |
@md |
28rem (448px) |
@lg |
32rem (512px) |
@xl |
36rem (576px) |
@2xl |
42rem (672px) |
You can customize these in your @theme inline block if the defaults don't match your layout.
A real example: a reusable product card
Here's the pattern we use in SaaSify and the Portfolio template. Same component, three contexts:
function ProductCard({ product }) {
return (
<div className="@container">
<article className="rounded-2xl border bg-card p-4 @sm:p-6 @lg:flex @lg:items-center @lg:gap-8">
<img
src={product.image}
className="aspect-square w-full rounded-lg object-cover
@sm:aspect-[4/3]
@lg:aspect-square @lg:w-48 @lg:flex-shrink-0"
/>
<div className="mt-4 @lg:mt-0">
<h3 className="text-lg font-semibold @md:text-xl @lg:text-2xl">
{product.name}
</h3>
<p className="mt-2 text-sm text-muted-foreground @md:text-base">
{product.description}
</p>
<div className="mt-4 @lg:mt-6">
<span className="text-2xl font-bold">${product.price}</span>
</div>
</div>
</article>
</div>
);
}
Drop this into any grid and it adapts:
// Full-width stacking mobile
<div className="space-y-4">
<ProductCard product={p} />
</div>
// 3-column grid on desktop β each cell renders at @md
<div className="grid grid-cols-3 gap-6">
<ProductCard product={p} />
</div>
// Narrow sidebar β stays at base size
<aside className="w-64">
<ProductCard product={p} />
</aside>
No media-query fiddling. No prop threading a size variant. Just the card, looking right everywhere.
Gotcha 1: The @container parent needs explicit sizing
This trips everyone. For container queries to work, the @container parent needs to have a width that the browser can measure. If the parent is inside a flex column with no explicit width, or inside a grid cell with min-content, container queries won't fire until a parent establishes sizing.
Usually this "just works" because most layouts have some grid or flex sizing. But when the parent has no size, add w-full or put it in a container with known width:
// Won't work inside a flex parent without sized children
<div className="flex">
<div className="@container">
<Card /> {/* @sm: never fires */}
</div>
</div>
// Works β explicit width
<div className="flex">
<div className="@container w-full">
<Card />
</div>
</div>
Gotcha 2: Container vs viewport variants are distinct
The @md: variant (container) and the md: variant (viewport) are different. Don't mix them thinking one falls back to the other.
// These are different
<div className="text-base md:text-lg @md:text-xl">
{/* md:text-lg fires at viewport 768px+ */}
{/* @md:text-xl fires at container 28rem+ */}
</div>
Pick one per component, and usually @ (container) wins for reusability.
Gotcha 3: Named containers for multi-level queries
If a component nests inside another @container, you might want children to query the outer container, not the nearest one. Tailwind v4 supports named containers for this:
<div className="@container/outer">
<div className="@container/inner">
<p className="@lg/outer:text-xl">
{/* Queries the outer, ignoring the inner */}
</p>
</div>
</div>
Most projects never need this β if you do, name your containers after the semantic role (@container/card, @container/sidebar) rather than position.
Performance notes
Container queries are implemented in the browser with container-type: inline-size (or size for two-axis queries). The reflow cost is real but small β Chrome's implementation handles thousands of container-query elements without frame drops.
Tailwind v4 only emits the container-type on elements you explicitly mark with @container, so you don't pay the cost globally.
Browser support
As of 2026:
- Chrome/Edge 105+ β
- Safari 16+ β
- Firefox 110+ β
Which is to say: everyone who updated Chrome in the last two years has them. The @container feature is firmly in the "can use without guilt" category.
When not to use container queries
For layout decisions that depend on user viewport (hamburger menu vs full nav), you still want viewport breakpoints. Container queries are for component-level responsiveness, not app-level layout.
Rule of thumb: if the component is reusable across multiple layout slots, use @container. If the decision is global (site header, main content column), use md:/lg: viewport breakpoints.
Migrating existing components
If you already have responsive components using viewport breakpoints, you don't have to migrate everything. A sensible refactor:
- Identify the 3-5 components you reuse in multiple layout contexts
- Wrap each in
@container - Replace
sm:β@sm:,md:β@md:inside those components - Test by dropping them into different layout slots
The rest of your app can stay viewport-based.
Wrapping up
Container queries in Tailwind v4 let one component serve full-width, grid-cell, and sidebar contexts without duplication or prop-drilled size variants. The three gotchas β sized parents, container vs viewport distinction, and named containers β account for 90% of "why isn't this working" moments. Once you internalize them, you'll wonder how you shipped responsive components before.
Every Craftly template uses @container for its primary card components. If you want to see real-world patterns, browse the catalog at getcraftly.dev.












