For over a decade, responsive web design meant one thing: media queries that respond to the viewport width. A card component would switch from horizontal to vertical layout at 768px because that was the breakpoint where the page happened to have a sidebar. But the card itself doesn’t know about the sidebar — it only knows about the viewport. If you move that same card into a narrower column, a modal, or a dashboard widget, the media query fires at the wrong breakpoint because the available space changed but the viewport didn’t. Container queries fix this fundamental mismatch by letting components respond to the size of their parent container instead of the viewport.
CSS Container Queries, specified in CSS Containment Level 3, introduce a new responsive primitive: @container rules that evaluate against a containment context rather than the viewport. A parent element declares itself as a container with container-type: inline-size, and child elements can query the container’s width with @container (min-width: 400px). The component adapts to its available space regardless of where it’s placed in the page — sidebar, main content, modal, or embedded widget.
Tailwind CSS v4 adds first-class support for container queries through the @container variant prefix. Instead of md:flex-row (viewport-based), you write @md:flex-row (container-based), and the component responds to its container’s width. The internal component library uses this extensively for sidebar-aware layouts where the main content area’s available width depends on whether a collapsible sidebar is open — a scenario where viewport media queries produce incorrect results.
A container query requires an ancestor element to establish a containment context via container-type: inline-size (or size for both axes). This tells the browser to track the element’s dimensions and make them queryable by descendants. The contained element establishes a new layout boundary — its size is determined by its own constraints and content, not by the queries of its children. This prevents circular dependencies where a child’s layout affects the container’s size, which would change the query result, which would change the child’s layout.
Media queries (@media (min-width: 768px)) check the viewport. Container queries (@container (min-width: 400px)) check the nearest ancestor with container-type set. The difference matters whenever a component’s available space differs from the viewport width — which is most of the time in real applications. Sidebars, multi-column layouts, modals, popovers, embedded widgets, and CMS content blocks all create contexts where viewport width is an unreliable proxy for available space.
When multiple containment contexts are nested, @container queries match the nearest one by default. Named containers (container-name: sidebar) let you query a specific ancestor: @container sidebar (min-width: 300px). This is essential in complex layouts where a component inside a sidebar card needs to query the sidebar’s width, not the card’s width. Tailwind v4 supports named containers with the @container/name syntax.
CSS introduces new units relative to the container’s dimensions: cqw (1% of container width), cqh (1% of container height), cqi (1% of inline size), and cqb (1% of block size). These units let you size elements proportionally to their container — a heading set to font-size: 3cqi scales with its container’s width, creating fluid typography without viewport-based clamp() calculations.
Container queries enable truly intrinsic component design — components that define their own responsive behavior based on the space they’re given, independent of the page layout. A ProductCard knows “below 300px I stack vertically, above 300px I go horizontal” regardless of whether it’s in a 3-column grid, a sidebar, or a modal. This makes components genuinely portable across layouts and applications.
The parent declares containment, and children query the container’s dimensions. This is the minimal CSS needed to make a component container-aware.
/* The parent establishes a containment context */
.content-area {
container-type: inline-size;
container-name: main;
}
/* The child queries the container, not the viewport */
.result-card {
display: flex;
flex-direction: column;
gap: 12px;
}
/* When the container is wide enough, switch to horizontal */
@container main (min-width: 500px) {
.result-card {
flex-direction: row;
align-items: center;
}
.result-card__image {
width: 200px;
flex-shrink: 0;
}
}
/* Full-width layout when container is very wide */
@container main (min-width: 800px) {
.result-card {
gap: 24px;
}
.result-card__meta {
display: flex;
gap: 16px;
}
}
Tailwind v4 provides the @container utility for declaring containment and the @ prefix for container-responsive variants, mirroring the familiar sm:/md:/lg: viewport breakpoints.
// Sidebar-aware layout in the component library
function DashboardLayout({ children }: { children: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = React.useState(true);
return (
<div className="flex h-screen">
{/* Collapsible sidebar */}
<aside className={cn(
"transition-all duration-300 border-r border-border",
sidebarOpen ? "w-64" : "w-16"
)}>
{/* sidebar content */}
</aside>
{/* Main content area is a container */}
<main className="flex-1 @container overflow-auto p-6">
{children}
</main>
</div>
);
}
// Components inside respond to main's width, not viewport
function SearchResults({ results }: { results: Result[] }) {
return (
<div className="grid gap-4 @sm:grid-cols-1 @md:grid-cols-2 @xl:grid-cols-3">
{results.map((result) => (
<ResultCard
key={result.id}
className="@container" {/* Each card is also a container */}
>
<div className="flex flex-col @xs:flex-row gap-3">
<img
src={result.thumbnail}
className="w-full @xs:w-24 @xs:h-24 rounded-md object-cover"
/>
<div className="flex-1">
<h3 className="font-semibold text-sm @sm:text-base">
{result.title}
</h3>
<p className="text-muted-foreground text-xs @sm:text-sm line-clamp-2">
{result.snippet}
</p>
</div>
</div>
</ResultCard>
))}
</div>
);
}
Named containers let deeply nested components query a specific ancestor, which is critical when containers are nested multiple levels deep.
/* CSS for named containers */
.dashboard-panel {
container-type: inline-size;
container-name: panel;
}
.dashboard-panel .widget {
container-type: inline-size;
container-name: widget;
}
/* Query the widget container (nearest) */
@container widget (min-width: 250px) {
.chart-legend {
display: flex;
flex-wrap: wrap;
}
}
/* Query the panel container (named, skips widget) */
@container panel (min-width: 600px) {
.widget-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Tailwind v4 named container syntax */
/* <div className="@container/panel"> → container-name: panel */
/* <div className="@md/panel:grid-cols-2"> → @container panel (min-width: ...) */
The internal component library’s primary use case for container queries is the sidebar-aware dashboard layout. The admin dashboard has a collapsible sidebar (64px collapsed, 256px expanded). When the sidebar toggles, the main content area’s width changes by ~192px — enough to shift a 3-column grid to 2 columns on a 1280px monitor. With viewport media queries, this transition wouldn’t happen because the viewport width hasn’t changed. Container queries on the main content area detect the actual available width and reflow the grid correctly.
The search results page uses nested container queries: the results grid is a container that determines column count, and each result card is also a container that determines whether to display in horizontal or vertical layout. A narrow viewport with no sidebar might show 1 column of horizontal cards, while a wide viewport with sidebar might show 2 columns of vertical cards — the same component in both cases, adapting to its actual space.
Embeddable widgets distributed to merchant sites rely entirely on container queries because the widget has no control over the viewport. A search bar widget might be placed in a 300px sidebar or a 900px hero section. Container queries let the widget adapt its layout, result count, and even the level of detail shown in autocomplete suggestions based on the container it’s placed in — making the widget truly portable across merchant sites without per-site CSS customization.
Container queries have broad browser support (Chrome 105+, Safari 16+, Firefox 110+), but older browsers need a graceful fallback. Use @supports to detect support and provide viewport-based defaults.
/* Mobile-first base (works everywhere) */
.product-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
/* Viewport fallback for browsers without container queries */
@media (min-width: 768px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1200px) {
.product-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Container query override for supporting browsers */
@supports (container-type: inline-size) {
.product-grid-container {
container-type: inline-size;
}
/* Reset the media query rules */
.product-grid {
grid-template-columns: 1fr;
}
@container (min-width: 500px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@container (min-width: 800px) {
.product-grid {
grid-template-columns: repeat(3, 1fr);
}
}
}
Container query units enable typography that scales with its container, creating fluid headings without viewport-relative calculations.
/* Container-relative fluid typography */
.hero-section {
container-type: inline-size;
}
.hero-section h1 {
/* Fluid between 24px and 48px based on container width */
font-size: clamp(1.5rem, 5cqi, 3rem);
line-height: 1.1;
}
.hero-section .subtitle {
font-size: clamp(0.875rem, 2cqi, 1.25rem);
max-width: 60cqi; /* 60% of container width */
}
/* Stats counter that scales with its container */
.stat-card {
container-type: inline-size;
}
.stat-card .number {
font-size: clamp(2rem, 8cqi, 4rem);
font-weight: 800;
font-variant-numeric: tabular-nums;
}
@container rules silently do nothing if no ancestor has container-type set. Unlike media queries that always have the viewport as context, container queries require explicit opt-in. If your container styles aren’t applying, check that the parent chain includes a containment context.container-type: size establishes containment on both axes, which prevents the element from sizing based on its content’s height. This can cause zero-height containers. Use container-type: inline-size (width only) unless you specifically need to query the container’s height.md: (viewport) and @md: (container) on the same component creates confusing behavior because they fire at different thresholds. Pick one model per component. Components placed in variable-width contexts should use container queries; page-level layout can continue using viewport queries.@container queries match the nearest containment context. In nested layouts, this might be a card instead of the panel you intended. Use named containers (container-name) in complex layouts to ensure queries target the correct ancestor.container-type: inline-size establishes a new layout containment boundary. Browsers handle this efficiently for typical use (dozens of containers), but setting containment on every element in a long list (thousands of items) can cause layout performance issues. Apply containment at meaningful layout boundaries, not on every child element.