07 · Container Queries in Tailwind¶
Breakpoints answer "how wide is the screen?". Components usually care about something else: "how much room do I have?". A product card in a three-column grid on a wide monitor might have less space than the same card alone on a tablet. With breakpoints, you'd have to know where the card will be placed and write different classes for each place. Container queries let the card adapt to its own container, so the same markup works anywhere.
Container queries are built into Tailwind v4 (in v3 they needed a plugin). For the underlying CSS, see Container Queries on the HTML & CSS course.
The two halves: a container and a query¶
<div class="@container">
<article class="flex flex-col gap-4 @md:flex-row">
<img class="aspect-video w-full rounded-lg object-cover @md:w-40" src="…" alt="">
<div>…</div>
</article>
</div>
@containermarks an element as a query container. It compiles tocontainer-type: inline-size;.@md:flex-rowapplies when the nearest ancestor container is at leastmdwide:
Notice that @md is 28rem, not the 48rem of the md: breakpoint. Container sizes
come from the --container-* theme scale, which is much smaller than the breakpoint
scale, because containers are usually smaller than screens:
| Variant | Size | Variant | Size | |
|---|---|---|---|---|
@3xs: |
16rem | @lg: |
32rem | |
@2xs: |
18rem | @xl: |
36rem | |
@xs: |
20rem | @2xl: |
42rem | |
@sm: |
24rem | @3xl: |
48rem | |
@md: |
28rem | … up to @7xl: |
80rem |
Measured: one component, two widths¶
We put that exact card twice on a 1280px-wide page: once in an 18rem sidebar (w-72)
and once in the flexible main column. Same classes, same viewport:
sidebar container: 288px wide -> flex-direction: column
main container: 920px wide -> flex-direction: row
With a breakpoint like lg:flex-row, both would have been rows (the screen is wide), and
the sidebar card would have squeezed its image and text into 288px.
Max and range queries¶
They work just like the breakpoint versions:
<nav class="@max-md:hidden">…</nav> <!-- container < 28rem -->
<div class="@sm:@max-lg:grid">…</div> <!-- 24rem ≤ width < 32rem -->
<div class="@min-[30rem]:p-4 @[30rem]:p-5">…</div> <!-- arbitrary sizes -->
@[30rem]: and @min-[30rem]: both compiled to @container (width >= 30rem).
Named containers¶
A query matches the nearest container ancestor. When containers are nested (a card inside a panel inside a page), you sometimes want to query an outer one. Name it:
<main class="@container/main">
<section class="@container">
<ul class="grid gap-4 @lg/main:grid-cols-3">…</ul>
</section>
</main>
.\@container\/main { container-type: inline-size; container-name: main; }
@container main (width >= 32rem) {
.\@lg\/main\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
Container query units¶
Inside a container you can size things relative to it with cqw (1% of the container's
width), cqi (1% of its inline size) and friends, via arbitrary values:
The heading scales with its card, clamped between 1rem and 1.75rem.
The containment pitfalls¶
container-type: inline-size tells the browser the element's width must not depend on
its contents (that's what makes querying it possible without an infinite loop). Two
consequences catch people out:
-
Shrink-to-fit containers collapse. An
@containerthat'sinline-block, a flex item with no width, a float, or an absolutely positioned box normally takes the width of its content. With size containment, its content counts as zero. We tested<div class="@container inline-block">Some content here</div>and it measured 0px wide. Give such containers an explicit width (w-full,w-72,flex-1) or put@containeron a block-level wrapper instead. -
An element can't query itself.
@md:looks at the nearest container ancestor. We put@container @md:bg-red-500on one 40rem-wide element and the background was not applied. Put@containeron a wrapper and the@md:classes on its children.
Containment doesn't change everything about the element, though. We checked a common
worry: a fixed child inside an @container (offset 200px from the page edge) was
still positioned against the viewport at top: 0; left: 0, exactly like one outside a
container. Inline-size containment is about sizing, not positioning.
Worked example: a reusable stat tile¶
<div class="@container">
<div class="grid gap-3 rounded-xl border border-gray-200 p-4 @xs:grid-cols-[auto_1fr]
@xs:items-center @md:p-6">
<span class="grid size-10 place-items-center rounded-lg bg-sky-50 text-sky-700
@md:size-12" aria-hidden="true">
<svg class="size-5" viewBox="0 0 20 20" fill="currentColor"><circle cx="10" cy="10" r="6"/></svg>
</span>
<div>
<p class="text-sm text-gray-500">Active users</p>
<p class="text-[clamp(1.25rem,8cqi,2.25rem)] font-semibold tabular-nums">1,284</p>
</div>
<p class="text-sm text-emerald-700 @md:col-span-2 @max-xs:hidden">+4.2% vs last week</p>
</div>
</div>
(The figures are sample data.) Put it in a narrow dashboard column and it stacks the icon above the number and hides the trend line. Put it in a wide column and the icon sits beside the number, the padding grows, and the trend appears. The page layout decides the width; the tile decides how to use it.
Breakpoints or container queries?¶
- Page-level layout (how many columns the page has, whether the sidebar is shown): breakpoints. The page really does depend on the screen.
- Component internals (does this card lay out horizontally, how big is this heading): container queries, so the component works wherever it's placed.
- Typography on long-form pages: usually breakpoints, or fluid
clamp()with viewport units.
How It Actually Works¶
@container is a utility that sets container-type (and container-name for the
/name form). The @{size} variants are generated from the --container-* theme
tokens: each one wraps the rule in @container (width >= <value>), and @max-{size}
uses <. Named versions put the name in the query: @container main (…). Like
breakpoint variants, they're sorted by size so larger queries override smaller ones.
At runtime the browser does all the work. When it lays out a container, it resolves the container's width first (that's why the width can't depend on content), then evaluates the queries for every descendant against that width. Each element finds its nearest matching container by walking up the tree, filtered by name if the query has one.
Common mistakes¶
- Expecting
@mdto mean 768px. It's 28rem; container sizes are their own scale. - Forgetting the
@containerancestor. Without one,@md:classes never apply. - Putting
@containerand@md:on the same element and expecting it to query itself. - Making an
inline-block, floated or unsized flex item a container, which collapses to zero width. - Using container queries for page layout that genuinely depends on the viewport.
- Nesting unnamed containers and querying the wrong one. Name the outer container.
Exercise¶
- Build the card from the first example and place it in a 2-column grid, a 4-column grid and a narrow sidebar on one page. Confirm with DevTools (containers show a badge in the Elements panel) which layout each copy uses.
- Reproduce the zero-width container: put
@containeron aninline-blockelement, then fix it. - Build the stat tile and make the number's size fluid with
cqiunits. Resize its column and watch it scale. - Nest a named page container and an unnamed card container, and use
@lg/main:on an element inside the card.