Tailwind CSS 48 🎨 Container Query Plugin Mechanics and Element-Based Responsiveness
Media queries ask one question: how wide is the browser window? Container queries ask a different question: how wide is this component’s parent? The distinction matters when a component appears in multiple layout contexts. A card in a sidebar and the same card in a main panel should look different, even on the same screen. Viewport breakpoints cannot express that distinction — they only know the window width, not the space the component actually has. Container queries solve this by scoping responsiveness to the element’s own containing block.
The @container class establishes an element as a query container, and the @sm:, @md:, @lg: variants style descendants based on that container’s width . The container breakpoints use their own scale, separate from the viewport breakpoints. The default container sizes match the max-width scale: @sm is 24rem (384px), @md is 28rem (448px), @lg is 32rem (512px), and so on . This is a critical distinction: md: triggers at 768px of viewport width, while @md: triggers at 448px of container width .
In Tailwind v4, container queries are built into the framework. The @tailwindcss/container-queries plugin was required in v3.2, but it is no longer needed in v4 . The syntax is the same: @container on the parent, @sm:, @md:, @lg: on the children. Named containers use @container/name and @md/name:, and max-width queries use @max-md: . The container query ranges stack @min-* and @max-* variants for precise control .
This chapter covers three areas. First, why container queries exist — the component-context problem and the limitation of viewport breakpoints. Second, how the @container class and the @*: variants work — the container scale, the named containers, the max-width queries, and the ranges. Third, the patterns and the integration — the card components, the sidebar layouts, the migration from viewport queries, and the coexistence with media queries. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the container query scoping.
Key point: Container queries style elements based on their parent’s width, not the viewport’s. The @container class marks the parent, and the @sm:, @md:, @lg: variants style the children. The container scale is separate from the viewport scale: @md is 448px, while md: is 768px. Named containers use @container/name and @md/name:. Max-width queries use @max-md:. In Tailwind v4, container queries are native; the plugin is only for v3.2+.
Why container queries exist
The component-context problem. A reusable component is designed once and placed in many contexts: a sidebar, a grid cell, a modal, a full-width panel. The component’s ideal layout depends on the width it is given, not the width of the browser window. A card in a 300px sidebar should stack vertically. The same card in a 600px panel should spread horizontally. A viewport breakpoint cannot express this because it only knows the window width. Container queries solve it by making the component respond to its actual available space .
The viewport-breakpoint limitation. The md: variant asks whether the viewport is at least 768px wide. It does not ask whether the component has enough space. A component in a narrow sidebar on a wide screen sees the md: variant as active, even though the component itself has only 250px of room. The container query variant @md: asks whether the container is at least 448px wide, which is the question that actually matters for the component’s layout .
The component-reusability problem. A component that uses viewport queries is coupled to the page’s layout. It assumes that when the viewport is md, it will have enough space. That assumption breaks when the component is placed in a narrow container on a wide screen. A component that uses container queries is decoupled from the page. It responds to the width it is given, wherever it is placed. The same component works in a sidebar, a grid cell, a modal, and a full-width panel without any changes .
The named-container problem. When containers are nested, a child’s @md: query targets the nearest ancestor container. If the child is inside a card, inside a sidebar, inside a main panel, the @md: query targets the card, not the main panel. Named containers disambiguate this. The parent is marked with @container/card, and the child uses @md/card: to target that specific container . This is essential for complex nested layouts.
The max-width and range problem. The @max-md: variant applies styles below the container breakpoint. The @min-md:@max-lg: combination applies styles only within a specific range. These are the tools for the precise control: hide an element in narrow containers, show it in medium ones, and change it again in wide ones .
The trade-off. Container queries introduce containment, which changes the element’s formatting context. This can affect absolutely positioned descendants and margin collapsing. The container must also have a constrained inline size; a full-width block with no max-width or grid/flex constraint will fire the container breakpoints at the same thresholds regardless of context, defeating the purpose . The trade-off is between the precision of the component-level responsiveness and the structural requirements of the containment.
a. The @container class and the container scale
The @container class marks an element as a query container. The descendants can then use the @sm:, @md:, @lg: variants.
<div class="@container">
<div class="flex flex-col @sm:flex-row @lg:grid @lg:grid-cols-3">
<!-- This responds to the container's width, not the viewport -->
</div>
</div>
The container scale is separate from the viewport scale. The default container sizes match the max-width scale, not the screens scale :
| Variant | Container width | Viewport equivalent |
|---|---|---|
@sm: | 24rem (384px) | sm: is 640px |
@md: | 28rem (448px) | md: is 768px |
@lg: | 32rem (512px) | lg: is 1024px |
@xl: | 36rem (576px) | xl: is 1280px |
@2xl: | 42rem (672px) | 2xl: is 1536px |
The distinction is the source of the most common migration confusion. A component that used md: in a viewport-based layout will not behave the same way with @md: because the thresholds are different. The @md: fires at 448px, while the md: fires at 768px .
The @container-normal class. The @container-normal class removes the containment, so the element stops acting as a container . This is useful for the conditional containment.
The container type. By default, the @container class uses container-type: inline-size, which queries the inline dimension (usually the width). The @container/size form queries both dimensions .
b. Named containers and max-width queries
Named containers. When containers are nested, the named containers disambiguate the target.
<main class="@container/main">
<aside class="@container/sidebar">
<nav class="@sm/sidebar:block @lg/main:flex">
<!-- Responds to the sidebar width for @sm/sidebar: -->
<!-- Responds to the main width for @lg/main: -->
</nav>
</aside>
</main>
The @container/main marks the main element as the container named main. The @container/sidebar marks the aside as the container named sidebar. The @sm/sidebar: targets the sidebar, and the @lg/main: targets the main. Without the names, the @sm: would target the nearest ancestor container, which is the sidebar .
Max-width queries. The @max-md: variant applies styles below the container breakpoint.
<div class="@container">
<nav class="@max-md:hidden">Desktop nav</nav>
<button class="@md:hidden">Menu</button>
</div>
The @max-md:hidden hides the element when the container is narrower than md (448px). The @md:hidden hides it when the container is at least md .
The container query ranges. Stacking @min-* and @max-* creates the ranges:
<div class="@container">
<div class="@sm:@max-lg:bg-blue-100">
Blue only when the container is between sm and lg
</div>
</div>
The @sm: and the @max-lg: conditions must both be true for the style to apply .
The arbitrary container sizes. The @[17.5rem]: syntax creates the one-off sizes:
<div class="@[17.5rem]:underline"></div>
The underline applies when the container is at least 17.5rem .
c. The patterns and the migration
The card component pattern. The card is the canonical container query use case. It stacks in narrow containers and spreads in wide ones.
<div class="@container">
<article class="flex flex-col @sm:flex-row @lg:grid @lg:grid-cols-3">
<img src="/cover.jpg" class="w-full @sm:w-32 @lg:w-full" />
<div class="@sm:ml-4 @lg:ml-0">
<h3 class="text-lg @sm:text-xl @lg:text-2xl">Title</h3>
<p class="text-sm @lg:text-base">Description</p>
</div>
</article>
</div>
The same card renders as a vertical stack in a 250px sidebar and as a horizontal layout in a 500px panel, without any knowledge of the viewport .
The sidebar layout pattern. The sidebar is the container, and the navigation responds to its width.
<aside class="@container/sidebar">
<nav class="flex flex-wrap gap-2 @sm/sidebar:gap-1">
<a href="#" class="@sm/sidebar:py-2">Link</a>
</nav>
</aside>
The navigation adapts to the sidebar’s width, not the viewport’s .
The migration from viewport queries. The migration is mechanical: replace sm: with @sm:, md: with @md:, and so on. But the breakpoint values are not 1:1. The viewport md is 768px, while the container @md is 448px. The migration requires choosing the different container breakpoints. The @sm (384px) and @md (448px) are the sweet spots for the card-style components, while the @lg (512px) works for the wider content blocks .
The coexistence with media queries. Container queries and media queries coexist without conflict. The media queries own the page-level layout: the navigation drawer, the global font sizes, the overall spacing. The container queries own the component-level layout: the card, the widget, the reusable component. The rule is: page layout = media queries; component layout = container queries .
<div class="grid grid-cols-1 md:grid-cols-3">
<!-- md: is the viewport query -->
<div class="@container">
<!-- @container marks the card's parent -->
<div class="flex flex-col @md:flex-row">
<!-- @md: is the container query -->
</div>
</div>
</div>
The md:grid-cols-3 is the viewport query: when the browser is at least 768px, the grid shows three columns. The @md:flex-row is the container query: when the grid cell is at least 448px, the card goes horizontal. The two operate at different levels .
Complete Example Session
<!-- ============================================
PART 1: THE BASIC CONTAINER QUERY
============================================ -->
<div class="@container">
<div class="block @lg:flex">
<!-- The flex applies when the container is at least 32rem -->
</div>
</div>
<!-- ============================================
PART 2: THE CARD COMPONENT
============================================ -->
<div class="@container">
<article class="flex flex-col @sm:flex-row @lg:grid @lg:grid-cols-3">
<img src="/cover.jpg" class="w-full @sm:w-32 @lg:w-full" />
<div class="@sm:ml-4 @lg:ml-0">
<h3 class="text-lg @sm:text-xl @lg:text-2xl">Title</h3>
<p class="text-sm @lg:text-base">Description</p>
</div>
</article>
</div>
<!-- ============================================
PART 3: NAMED CONTAINERS
============================================ -->
<main class="@container/main">
<aside class="@container/sidebar">
<nav class="@sm/sidebar:block @lg/main:flex">
<!-- @sm/sidebar: targets the sidebar -->
<!-- @lg/main: targets the main -->
</nav>
</aside>
</main>
<!-- ============================================
PART 4: MAX-WIDTH QUERIES
============================================ -->
<div class="@container">
<nav class="@max-md:hidden">Desktop nav</nav>
<button class="@md:hidden">Menu</button>
</div>
<!-- ============================================
PART 5: CONTAINER QUERY RANGES
============================================ -->
<div class="@container">
<div class="@sm:@max-lg:bg-blue-100 @lg:bg-white">
Blue in medium containers, white in large
</div>
</div>
<!-- ============================================
PART 6: ARBITRARY CONTAINER SIZES
============================================ -->
<div class="@container">
<div class="@[17.5rem]:underline">
Underlined when the container is at least 17.5rem
</div>
</div>
<!-- ============================================
PART 7: THE SIDEBAR LAYOUT
============================================ -->
<aside class="@container/sidebar">
<nav class="flex flex-wrap gap-2 @sm/sidebar:gap-1">
<a href="#" class="@sm/sidebar:py-2">Link</a>
</nav>
</aside>
<!-- ============================================
PART 8: THE PRODUCT CARD
============================================ -->
<div class="@container">
<div class="flex flex-col gap-4 p-4 @sm:flex-row">
<img src="/product.jpg" class="w-full rounded @sm:w-32 @md:w-48" />
<div class="flex-1">
<h3 class="text-lg font-semibold @md:text-xl">Product Name</h3>
<p class="mt-1 text-sm text-gray-600 @lg:text-base">Description</p>
</div>
</div>
</div>
<!-- ============================================
PART 9: THE VIEWPORT + CONTAINER COEXISTENCE
============================================ -->
<div class="grid grid-cols-1 md:grid-cols-3">
<!-- md: is the viewport query -->
<div class="@container">
<!-- @container marks the grid cell -->
<div class="flex flex-col @md:flex-row">
<!-- @md: is the container query -->
</div>
</div>
</div>
<!-- ============================================
PART 10: THE COMPLETE DASHBOARD
============================================ -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- The sidebar: viewport-driven column, container-driven content -->
<aside class="md:col-span-1 @container/sidebar">
<nav class="flex flex-row md:flex-col gap-2 @sm/sidebar:gap-3">
<a href="#" class="text-sm @sm/sidebar:text-base">Dashboard</a>
<a href="#" class="text-sm @sm/sidebar:text-base">Settings</a>
</nav>
</aside>
<!-- The main: viewport-driven column, container-driven cards -->
<main class="md:col-span-3 @container/main">
<div class="grid grid-cols-1 @sm/main:grid-cols-2 @lg/main:grid-cols-3 gap-4">
<div class="card @container">
<div class="flex flex-col @md:flex-row">
<!-- The card adapts to its own width -->
</div>
</div>
</div>
</main>
</div>
The ten parts show the basic container query, the card component, the named containers, the max-width queries, the ranges, the arbitrary sizes, the sidebar layout, the product card, the viewport and container coexistence, and the complete dashboard.
Quick Reference
The Container Query Syntax
| Syntax | Meaning |
|---|---|
@container | Marks the element as a container |
@sm:, @md:, @lg: | Container min-width variants |
@max-md: | Container max-width variant |
@sm:@max-lg: | Container range |
@container/name | Named container |
@md/name: | Variant targeting the named container |
@[17.5rem]: | Arbitrary container size |
@container-normal | Removes the containment |
The Container Scale vs the Viewport Scale
| Container | Width | Viewport | Width |
|---|---|---|---|
@sm: | 384px | sm: | 640px |
@md: | 448px | md: | 768px |
@lg: | 512px | lg: | 1024px |
@xl: | 576px | xl: | 1280px |
The Common Patterns
| Pattern | The classes |
|---|---|
| The card | flex flex-col @sm:flex-row @lg:grid |
| The sidebar | @container/sidebar + @sm/sidebar: |
| The hide/show | @max-md:hidden + @md:hidden |
| The range | @sm:@max-lg:bg-blue-100 |
| The coexistence | md:grid-cols-3 + @md:flex-row |
The v4 vs the v3
| Aspect | v3.2+ | v4 |
|---|---|---|
| The plugin | @tailwindcss/container-queries | Native |
| The config | tailwind.config.js | @theme |
| The max-width | The extended plugin | Native @max-* |
Best Practices
✅ Do This:
<!-- Use @container on the layout parent -->
<div class="@container"><article class="@md:flex-row"> <!-- ✅ -->
<!-- Use the named containers for the nesting -->
<div class="@container/sidebar"><nav class="@sm/sidebar:block"> <!-- ✅ -->
<!-- Use the max-width for the hide/show -->
<nav class="@max-md:hidden">Desktop nav</nav> <!-- ✅ -->
<!-- Use the ranges for the precise control -->
<div class="@sm:@max-lg:bg-blue-100"> <!-- ✅ -->
<!-- Use the container queries for the components -->
<article class="@md:grid-cols-2"> <!-- ✅ -->
<!-- Use the media queries for the page layout -->
<div class="md:grid-cols-3"> <!-- ✅ -->
❌ Don’t Do This:
<!-- Don't put @container on every div -->
<div class="@container"><div class="@container"> // ❌
<!-- Don't assume @md equals md -->
@md: fires at 448px; md: fires at 768px // ❌
<!-- Don't use the container queries for the page layout -->
<div class="@md:grid-cols-3"> <!-- use md: for the page --> // ❌
<!-- Don't forget the container has a bounded width -->
<div class="@container w-full"> <!-- no max-width or flex constraint --> // ❌
<!-- Don't nest the containers without the names -->
<div class="@container"><div class="@container"><div class="@md:..."> // ❌
<!-- Don't use the plugin in v4 -->
@plugin "@tailwindcss/container-queries"; <!-- native in v4 --> // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The query does not fire | The container has no bounded width | Add max-width, grid, or flex constraint |
| The query fires at the wrong width | Confusing @md with md | @md is 448px; md is 768px |
| The nested query targets the wrong container | No names | Use @container/name and @md/name: |
| The absolute positioning breaks | The containment changes the formatting context | Check the positioned descendants |
| The margin collapsing changes | The containment | Check the margins |
| The plugin is installed in v4 | The v4 has the native support | Remove the plugin |
| The query does not work in v3 | The plugin is missing | Install @tailwindcss/container-queries |
Real-World Examples
1. The Basic Container
<div class="@container"><div class="@lg:flex"></div></div>
2. The Card
<article class="flex flex-col @sm:flex-row @lg:grid @lg:grid-cols-3">
3. The Named Container
<aside class="@container/sidebar"><nav class="@sm/sidebar:block">
4. The Max-Width
<nav class="@max-md:hidden">Desktop nav</nav>
5. The Range
<div class="@sm:@max-lg:bg-blue-100">
6. The Arbitrary Size
<div class="@[17.5rem]:underline">
7. The Sidebar Layout
<aside class="@container/sidebar"><nav class="flex flex-wrap @sm/sidebar:gap-1">
8. The Product Card
<div class="flex flex-col @sm:flex-row"><img class="w-full @sm:w-32">
9. The Coexistence
<div class="md:grid-cols-3"><div class="@container"><div class="@md:flex-row">
10. The Dashboard
<aside class="@container/sidebar"><main class="@container/main">
Visual
The Container Query Scoping
┌──────────────────────────────────────────────────────────────┐
│ THE CONTAINER QUERY SCOPING │
│ │
│ <div class="@container"> │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The @container marks the query container. │ │
│ │ │ │
│ │ <article class="@sm:flex-row @lg:grid"> │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ The @sm: and @lg: query the CONTAINER's width.│ │ │
│ │ │ Not the viewport's. │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The container width is the reference, not the viewport. │
│ │
└──────────────────────────────────────────────────────────────┘
The Container vs Viewport Scale
┌──────────────────────────────────────────────────────────────┐
│ THE CONTAINER vs VIEWPORT SCALE │
│ │
│ Container: @sm: 384px @md: 448px @lg: 512px │
│ Viewport: sm: 640px md: 768px lg: 1024px │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The @md: fires at 448px of CONTAINER width. │ │
│ │ The md: fires at 768px of VIEWPORT width. │ │
│ │ │ │
│ │ The two scales are SEPARATE. │ │
│ │ Do not assume the same thresholds. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Named Containers
┌──────────────────────────────────────────────────────────────┐
│ THE NAMED CONTAINERS │
│ │
│ <main class="@container/main"> │
│ <aside class="@container/sidebar"> │
│ <nav class="@sm/sidebar:block @lg/main:flex"> │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ @sm/sidebar: targets the SIDEBAR container. │ │
│ │ @lg/main: targets the MAIN container. │ │
│ │ │ │
│ │ Without the names, @sm: would target the nearest │ │
│ │ ancestor container, which is the sidebar. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Viewport + Container Coexistence
┌──────────────────────────────────────────────────────────────┐
│ THE VIEWPORT + CONTAINER COEXISTENCE │
│ │
│ <div class="grid grid-cols-1 md:grid-cols-3"> │
│ <!-- md: is the VIEWPORT query --> │
│ <div class="@container"> │
│ <!-- @container marks the grid cell --> │
│ <div class="flex flex-col @md:flex-row"> │
│ <!-- @md: is the CONTAINER query --> │
│ </div> │
│ </div> │
│ </div> │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The md:grid-cols-3 responds to the viewport. │ │
│ │ The @md:flex-row responds to the grid cell. │ │
│ │ │ │
│ │ The two coexist without conflict. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
@container | Marks the query container |
@sm:, @md:, @lg: | Container min-width variants |
@max-md: | Container max-width variant |
@sm:@max-lg: | Container range |
@container/name | Named container |
@md/name: | Variant targeting the named container |
@[17.5rem]: | Arbitrary container size |
| The container scale | @md is 448px |
| The viewport scale | md: is 768px |
| The v4 support | Native |
Key takeaways:
- Container queries style elements based on their parent’s width, not the viewport’s. The
@containerclass marks the query container, and the@sm:,@md:,@lg:variants style the descendants. The component adapts to the space it is given, wherever it is placed . - The container scale is separate from the viewport scale. The
@md:variant fires at 448px of container width, while themd:variant fires at 768px of viewport width. The migration from the viewport queries requires choosing the different container breakpoints . - Named containers disambiguate the nested queries. The
@container/namemarks the named container, and the@md/name:targets it. Without the names, the query targets the nearest ancestor container . - The
@max-*variants and the ranges provide the precise control. The@max-md:applies below the breakpoint, and the@sm:@max-lg:applies within the range. These are the tools for the hide/show and the context-specific styles . - The container queries and the media queries coexist. The media queries own the page-level layout: the navigation drawer, the global font sizes, the overall spacing. The container queries own the component-level layout: the card, the widget, the reusable component. The rule is: page layout = media queries; component layout = container queries .
- The container must have a bounded width. A full-width block with no
max-width,grid, orflexconstraint will fire the container breakpoints at the same thresholds regardless of context. The container needs the constrained inline size for the queries to be meaningful . - The containment changes the formatting context. The
@containerclass can affect the absolutely positioned descendants and the margin collapsing. The impact is usually invisible, but it is worth checking in the complex layouts . - The v4 has the native support. The
@tailwindcss/container-queriesplugin is required in v3.2, but it is no longer needed in v4. The syntax is the same, and the max-width queries are built in .
Remember: Container queries are the component-level responsiveness. The @container class marks the parent, and the @sm:, @md:, @lg: variants style the children based on the parent’s width. The container scale is separate from the viewport scale, the named containers disambiguate the nested queries, and the max-width queries handle the below-breakpoint styles. The media queries own the page layout, and the container queries own the component layout. The result is the components that adapt to their actual available space, which is the foundation of the truly reusable UI.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!