Tailwind v4 @container — Responsive Components That Hold Up in Sidebars and Grids Alike
At some point in responsive UI work, you hit a wall. A card component you carefully crafted works perfectly in the main feed, but the moment you drop the same code into a sidebar, images and text get squashed. You can slice your breakpoints ever finer, but a media query that measures the viewport has no way to know "how narrow a space is this component sitting in right now."
My first thought was "can't I just write more granular media queries and call it done?" — but actually using container queries changed how I think about component design altogether. Container queries are a CSS feature that lets a component be aware of the space it occupies, and starting with Tailwind v4 they're integrated as a core API with no separate plugin required. Let's take a look at what changes and how.
What this article covers
- How
@containerworks and how it differs from media queries- What changed in v4.0/v4.3 and the difference between
container-type: inline-sizeandsize- Four examples: basic card → named container →
@max-*→ dashboard widget- Three real-world pitfalls that actually tripped me up
Core Concepts
How Container Queries Work
The underlying mechanism is straightforward. Attach the @container class to a parent element to declare a query container, and the browser forms a context that communicates that element's size to its children. Children can then respond to that container width using variants prefixed with @, like @sm:, @md:, and @lg:.
<!-- Declare @container on the parent -->
<div class="@container">
<!-- Lay out horizontally when the nearest ancestor (@container) is 448px or wider -->
<div class="flex flex-col @md:flex-row gap-4">
...
</div>
</div>Viewport vs. container:
md:flex-rowapplies when the browser window is 768px or wider;@md:flex-rowapplies when the@containerelement wrapping this element is 448px or wider. The reference point is entirely different.
There's a part that's confusing at first glance: the threshold for @md: differs from the regular md:. This is intentional — the container values are deliberately smaller.
| Variant | Container threshold | Media query threshold |
|---|---|---|
@sm: / sm: |
384px | 640px |
@md: / md: |
448px | 768px |
@lg: / lg: |
512px | 1024px |
@xl: / xl: |
576px | 1280px |
Components are usually placed in spaces much narrower than the full viewport, so this makes sense. I remember spending a long time debugging early on because I didn't know this and kept wondering why @lg: wasn't firing.
What Changed in v4
In the v3 era you had to install the @tailwindcss/container-queries plugin separately. Here's a summary of changes by version:
- v4.0 (January 2025): Container queries built in.
@container,@max-*, and arbitrary values (@[500px]:) all available without a plugin - v4.1 (April 2025): Stability and performance improvements
- v4.3.0 (May 2026):
container-sizeutility added — from this version you can query both width and height simultaneously
One thing worth knowing: Tailwind's @container class sets container-type: inline-size internally. In this mode, only width can serve as the query basis — height is excluded. If you need height-based queries, you'll need to use the container-size utility added in v4.3.0, which sets container-type: size and makes both width and height available as query targets.
CSS Containment: Declaring
@containercauses the element to form a CSS Containment context. The browser isolates that element's layout calculation, computes its size, and passes that information down to child elements. That's why an ancestor@containermust be present for child variants like@md:to work.container-type: inline-size(the default) includes only horizontal size in the container context;sizeincludes both horizontal and vertical.
Browser support is solid enough. With Chrome 105+, Firefox 110+, and Safari 16+ coverage at over 93%, there's no significant barrier to production adoption.
Practical Application
Let's look at how these concepts play out in real work, starting with the simplest example and gradually increasing complexity.
Example 1: Basic Container Query — The Simplest Form
When introducing container queries for the first time, you can start with just @container and @md:flex-row — no names, no arbitrary values.
<div class="@container">
<div class="flex flex-col @md:flex-row gap-4 p-4 bg-white rounded-lg shadow">
<img
class="w-full rounded-lg object-cover"
src="/product.jpg"
alt="Product image"
>
<div>
<h3 class="text-lg font-bold">Product name</h3>
<p class="text-sm text-gray-600">Product description goes here.</p>
</div>
</div>
</div>Below 448px (@md:), the container stacks vertically; at 448px and above, it spreads horizontally. Whether you place this card in a narrow sidebar or a wide main area, it adjusts itself to the available space — behavior that was awkward to achieve with media queries.
Example 2: Named Containers for Production-Ready Cards
This is the pattern you'll encounter most often in real work. The same card needs to fit in a three-column grid, a narrow sidebar, and inside a modal. Naming the container lets you combine it with arbitrary breakpoints for much more precise control.
<div class="@container/card">
<div class="flex flex-col @[480px]/card:flex-row @[750px]/card:items-center gap-4 p-4">
<img
class="w-full @[480px]/card:w-32 rounded-lg object-cover"
src="/product.jpg"
alt="Product image"
>
<div class="flex-1">
<h3 class="text-lg @[750px]/card:text-2xl font-bold">Product name</h3>
<p class="text-sm text-gray-600">Product description goes here.</p>
</div>
</div>
</div>| Class | Role |
|---|---|
@container/card |
Declares a query container named card |
@[480px]/card:flex-row |
Horizontal layout when card container is 480px or wider |
@[750px]/card:items-center |
Adds vertical center alignment at 750px or wider |
@[480px]/card:w-32 |
Fixed image width applied only when the container is wide |
@[750px]/card:text-2xl |
Larger heading size at 750px or wider |
You can drop this component into any layout without touching the CSS. Adding the name (/card) may seem tedious, but that name pays off the moment you have nested structures.
<!-- Outer container -->
<div class="@container/main">
<!-- Inner nested container -->
<aside class="@container/sidebar">
<nav class="flex flex-col @sm/main:flex-row @sm/sidebar:flex-col">
<!-- Responds independently to /main and /sidebar -->
</nav>
</aside>
</div>The @container/{name} + @{size}/{name}: pattern lets you precisely target the ancestor container you want. This pattern is especially useful on complex pages like dashboards.
Example 3: @max-* for Handling Narrow Widths First
Honestly, I initially tried to get by with mobile-first only and no @max-* — but when a component's defaults are already set to the wide side and I needed to override for narrow widths, the reverse-direction query was much easier to read.
@max-* expresses "when this container is smaller than a certain size." It's the opposite direction from mobile-first (building up with @sm:, @md:), and the choice is simple: if the default state is defined for the wide layout and you only need exceptions for narrow widths, @max-* reads more clearly. Conversely, if the default state is the narrow one, building up with @sm:, @md: is more natural.
<div class="@container">
<!-- Small text when container is under 448px, default size otherwise -->
<p class="@max-md:text-sm text-base leading-relaxed">
Font size changes based on container width.
</p>
<!-- Button label that simplifies only when narrow -->
<button class="@max-sm:px-2 @max-sm:text-xs px-4 py-2 text-sm bg-blue-500 text-white rounded">
<span class="@max-sm:hidden">Buy Now</span>
<span class="@sm:hidden">Buy</span>
</button>
</div>Example 4: Dashboard Widget — Letting the Container Decide Its Own Layout
When I first tried this pattern, it was a "so that's why container queries exist" moment. The same widget needs a completely different layout when spanning one column versus two in a grid — and you can let the widget decide that itself without touching the page layout code.
<!-- Narrow when taking 1 column in grid, wide when taking 2 -->
<div class="@container/widget col-span-1 md:col-span-2">
<div class="flex flex-col @lg/widget:flex-row gap-4 p-6 bg-white rounded-xl shadow">
<!-- Summary metrics area -->
<div class="@lg/widget:w-1/3 bg-blue-50 rounded-lg p-4">
<p class="text-4xl font-bold text-blue-600">1,284</p>
<p class="text-sm text-gray-500 mt-1">Visitors this month</p>
<p class="text-xs text-green-600 mt-2">↑ 12% vs last month</p>
</div>
<!-- Detailed data table -->
<div class="@lg/widget:w-2/3">
<table class="w-full text-sm">
<thead>
<tr class="border-b text-left text-gray-500">
<th class="pb-2">Page</th>
<th class="pb-2">Visits</th>
<th class="pb-2">Bounce Rate</th>
</tr>
</thead>
<tbody>
<tr class="border-b">
<td class="py-2">/home</td>
<td>512</td>
<td>34%</td>
</tr>
<tr class="border-b">
<td class="py-2">/docs</td>
<td>389</td>
<td>21%</td>
</tr>
<tr>
<td class="py-2">/pricing</td>
<td>383</td>
<td>45%</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>When col-span-1 (widget is narrow), the metrics and table stack vertically. As it expands to col-span-2, the moment the /widget container exceeds the @lg threshold (512px), it automatically spreads out horizontally. The page layout code only decides the number of grid columns; internal widget arrangement is handled entirely by the widget itself.
Pros and Cons
From personal experience, container queries have the most dramatic effect in projects with heavy component reuse. Of the downsides, two actually tripped me up in production — working around height queries and debugging nested containers.
Advantages
- Component portability: Behaves consistently regardless of whether it's in a sidebar, grid, or modal
- No plugin required: Built in since v4 — no separate package to install or maintain
@max-*support: Reverse-direction (maximum-width) queries handled in a single class- Named containers: Precisely target the desired ancestor container in nested structures
- Coexists with media queries:
@mediaand@containerare independent CSS rules that can be combined without conflict
Disadvantages and Caveats
| Item | Details | Workaround |
|---|---|---|
| No height query variants | @container (default) uses container-type: inline-size, so only width is queryable |
Use v4.3.0's container-size class to set container-type: size, then use arbitrary variants like @[height>200px]: |
| Container declaration required | An ancestor @container must be present for queries to work |
Accept adding a wrapper div or redesign the layout structure |
| Debugging complexity | Tracking which container is the reference becomes difficult with many nested containers | Use strict naming and leverage Chrome DevTools' container panel |
| No support in legacy browsers | Only supported in modern browsers: Chrome 105+, etc. | Consider polyfills or progressive enhancement if legacy support is needed |
The Most Common Mistakes in Practice
- Using
@md:without@container— if no ancestor has@container, the variant is simply ignored. If something isn't applying, check the parent tree first. - Confusing breakpoints between
md:and@md:—md:is 768px,@md:is 448px. Forget they're different in a component that uses both and you'll get unexpected layout breaks. - Trying to replace all responsive behavior with container queries — things like header height and whether a sidebar is present, which determine the overall page structure, are still more natural with media queries. Splitting responsibilities as "page layout uses
md:/lg:; component internals use@md:/@lg:" makes maintenance much easier.
Closing Thoughts
Container queries solve a fundamental limitation of reusability and portability by letting components be aware of the space they occupy. They're available in Tailwind v4 without any separate installation, and browser support is solid, so now is a good time to start.
Three steps you can take right now:
-
Try upgrading Tailwind to v4.
The official migration docs walk you through v3 → v4 changes step by step. -
Wrap your most reused card or list item with
@container.
Experimenting while visually inspecting container boundaries in Chrome DevTools' container query panel will help you develop intuition much faster. -
Place the same component in a context with varying widths — like a sidebar or a modal.
The moment the layout adapts on its own without changing a single line of CSS, you'll feel firsthand why this paradigm is getting so much attention.
References
- Tailwind CSS v4.0 Official Blog | tailwindcss.com
- Responsive design Official Docs | tailwindcss.com
- Tailwind CSS v4 Container Queries: Modern Responsive Design | SitePoint
- Component-First Responsive Design with Tailwind v4 | Kickstage
- Container Queries in Tailwind CSS | Frontend Notes
- Replace Complex Media Queries With Tailwind Container Queries | Strapi
- Build Smarter Responsive UIs with Tailwind CSS 4 Container Queries | Medium
- CSS Container Queries: A Practical Guide 2026 | Mantlr
- GitHub — tailwindlabs/tailwindcss-container-queries (legacy plugin)