Privacy Policy© 2026 DEV BAK - TECH BLOG. All rights reserved.
DEV BAK - TECH BLOG
frontend

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 @container works and how it differs from media queries
  • What changed in v4.0/v4.3 and the difference between container-type: inline-size and size
  • 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:.

html
<!-- 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-row applies when the browser window is 768px or wider; @md:flex-row applies when the @container element 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-size utility 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 @container causes 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 @container must be present for child variants like @md: to work. container-type: inline-size (the default) includes only horizontal size in the container context; size includes 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.

html
<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.

html
<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.

html
<!-- 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.

html
<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.

html
<!-- 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: @media and @container are 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

  1. Using @md: without @container — if no ancestor has @container, the variant is simply ignored. If something isn't applying, check the parent tree first.
  2. 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.
  3. 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:

  1. Try upgrading Tailwind to v4.
    The official migration docs walk you through v3 → v4 changes step by step.

  2. 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.

  3. 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)
#TailwindCSS#컨테이너쿼리#반응형UI#CSS-Containment#미디어쿼리#TailwindV4#프론트엔드#컴포넌트설계#그리드레이아웃#대시보드UI
Share

Table of Contents

Core ConceptsHow Container Queries WorkWhat Changed in v4Practical ApplicationExample 1: Basic Container Query — The Simplest FormExample 2: Named Containers for Production-Ready CardsExample 3:Example 4: Dashboard Widget — Letting the Container Decide Its Own LayoutPros and ConsAdvantagesDisadvantages and CaveatsThe Most Common Mistakes in PracticeClosing ThoughtsReferences

Recommended Posts

TanStack DB in Practice: How a Client-Side DB Changes Optimistic Updates
frontend

TanStack DB in Practice: How a Client-Side DB Changes Optimistic Updates

Honestly, when I first heard "client-side embedded DB," my reaction was "just another buzzword." TanStack Query already handles server state management well — w...

June 23, 202617 min read
Implementing Cloudflare Workers for Edge MFE Orchestration to Reduce TTFB
frontend

Implementing Cloudflare Workers for Edge MFE Orchestration to Reduce TTFB

Honestly, when I first heard about micro-frontends (MFE), my immediate thought was, "Independent deployments per team? Sounds great — but doesn't that mean the ...

June 23, 202623 min read
15 Frontend Accessibility Checklist Items After EAA Takes Effect (European Accessibility Act WCAG 2.1 AA Standard)
frontend

15 Frontend Accessibility Checklist Items After EAA Takes Effect (European Accessibility Act WCAG 2.1 AA Standard)

On June 28, 2025, I stopped while reading a message that came through on Slack: "EAA has taken effect — is our service okay?" At first I thought, "It's an EU la...

June 26, 202626 min read
When Linting Gets 62x Faster, Your Development Habits Change — Migrating from ESLint to Oxlint in a Vite Project
frontend

When Linting Gets 62x Faster, Your Development Habits Change — Migrating from ESLint to Oxlint in a Vite Project

Have you ever felt that your linter is slow? I did too. I used to think waiting nearly 30 seconds every time I ran on a mid-sized React project was just normal...

June 23, 202619 min read
Fixing Micro Frontend Style Conflicts with CSS @layer | Cascade Layers Isolation Strategy
frontend

Fixing Micro Frontend Style Conflicts with CSS @layer | Cascade Layers Isolation Strategy

When I first worked on a Micro Frontend (MFE) project, I remember being quite surprised to see a button style deployed by Team A showing up altered on Team B's ...

June 23, 202622 min read
We Migrated from Webpack to Rsbuild and Production Builds Got 74% Faster — The Migration Reality and Rspack Pitfalls
frontend

We Migrated from Webpack to Rsbuild and Production Builds Got 74% Faster — The Migration Reality and Rspack Pitfalls

Honestly, my first reaction was "another new build tool?" — same as when Vite came out, same as when Turbopack was announced. But after seeing the case from Ala...

June 23, 202619 min read