CSS Container Queries
Container queries allow you to style elements based on the size of their parent container rather than the viewport. This is a paradigm shift from media queries — instead of asking "how wide is the screen?", you ask "how wide is my container?".
This enables truly reusable, context-aware components that adapt to wherever they are placed in a layout. A card component can rearrange itself whether it sits in a narrow sidebar or a wide main content area, without any viewport-based breakpoints.
| 1 | .card-container { |
| 2 | container-type: inline-size; |
| 3 | container-name: card; |
| 4 | } |
| 5 | |
| 6 | @container card (min-width: 400px) { |
| 7 | .card { |
| 8 | display: grid; |
| 9 | grid-template-columns: 1fr 2fr; |
| 10 | gap: 16px; |
| 11 | } |
| 12 | } |
| 13 | |
| 14 | @container card (max-width: 399px) { |
| 15 | .card { |
| 16 | display: flex; |
| 17 | flex-direction: column; |
| 18 | } |
| 19 | } |
The container-typeproperty establishes an element as a query container. It tells the browser to track the container's size so that child elements can query it.
| 1 | .element { |
| 2 | container-type: normal; /* default — no size containment */ |
| 3 | container-type: inline-size; /* tracks inline-axis size (width) */ |
| 4 | container-type: size; /* tracks both inline and block axis */ |
| 5 | } |
inline-size is the most common value. It applies containment only on the inline axis, allowing the element to still grow vertically based on its content. size applies containment to both axes, which may cause content to overflow if not managed carefully.
warning
The container-name property gives your container a name so that @container rules can target it specifically. This is essential when you have nested containers and need to query a particular ancestor.
| 1 | .sidebar-panel { |
| 2 | container-type: inline-size; |
| 3 | container-name: sidebar; |
| 4 | } |
| 5 | |
| 6 | .main-content { |
| 7 | container-type: inline-size; |
| 8 | container-name: content; |
| 9 | } |
| 10 | |
| 11 | /* Query the sidebar container specifically */ |
| 12 | @container sidebar (min-width: 300px) { |
| 13 | .widget { font-size: 1rem; } |
| 14 | } |
| 15 | |
| 16 | /* Query the content container */ |
| 17 | @container content (min-width: 600px) { |
| 18 | .widget { font-size: 1.25rem; } |
| 19 | } |
You can also assign multiple names to a single container using a space-separated list:
| 1 | .panel { |
| 2 | container-type: inline-size; |
| 3 | container-name: panel widget-area; |
| 4 | } |
The container shorthand combines both properties:
| 1 | .sidebar { |
| 2 | container: sidebar / inline-size; |
| 3 | } |
| 4 | |
| 5 | /* Equivalent to: */ |
| 6 | .sidebar { |
| 7 | container-type: inline-size; |
| 8 | container-name: sidebar; |
| 9 | } |
The @container at-rule works similarly to @media, but queries the nearest container with container-type applied — or a named container if specified.
| 1 | /* Query the nearest container */ |
| 2 | @container (min-width: 400px) { |
| 3 | .child { color: #00FF41; } |
| 4 | } |
| 5 | |
| 6 | /* Query a named container */ |
| 7 | @container card (min-width: 300px) { |
| 8 | .title { font-size: 1.5rem; } |
| 9 | } |
| 10 | |
| 11 | /* Query container style (future Capability Query) */ |
| 12 | @container card (style(--theme: dark)) { |
| 13 | .body { background: #0D0D0D; } |
| 14 | } |
Container queries support the same comparison operators as media queries: min-width, max-width, and range syntax:
| 1 | /* Range syntax */ |
| 2 | @container (300px <= inline-size < 600px) { |
| 3 | .card { grid-template-columns: 1fr; } |
| 4 | } |
| 5 | |
| 6 | /* Equivalent to: */ |
| 7 | @container (min-width: 300px) and (max-width: 599px) { |
| 8 | .card { grid-template-columns: 1fr; } |
| 9 | } |
Container queries introduce six new length units that represent percentages of the container's dimensions. These work similarly to viewport units (vw, vh) but are relative to the nearest container rather than the viewport.
| Unit | Relative To | Description |
|---|---|---|
| cqw | Container Width | 1% of the container's inline size |
| cqh | Container Height | 1% of the container's block size |
| cqi | Container Inline-Size | 1% of the container's inline size (text-direction aware) |
| cqb | Container Block-Size | 1% of the container's block size (text-direction aware) |
| cqmin | Container Min | The smaller of cqi and cqb |
| cqmax | Container Max | The larger of cqi and cqb |
| 1 | .card { |
| 2 | container: card / inline-size; |
| 3 | } |
| 4 | |
| 5 | .card-title { |
| 6 | /* Font size scales with container width */ |
| 7 | font-size: clamp(1rem, 4cqi, 2.5rem); |
| 8 | } |
| 9 | |
| 10 | .card-image { |
| 11 | /* Maintain aspect ratio relative to container */ |
| 12 | aspect-ratio: 16 / 9; |
| 13 | width: 100%; |
| 14 | height: auto; |
| 15 | } |
| 16 | |
| 17 | .card-body { |
| 18 | /* Padding scales with container */ |
| 19 | padding: 2cqi; |
| 20 | } |
| 21 | |
| 22 | .card-icon { |
| 23 | /* Size relative to container min dimension */ |
| 24 | width: 8cqmin; |
| 25 | height: 8cqmin; |
| 26 | } |
Understanding when to use each approach is critical. Media queries respond to the viewport; container queries respond to a parent element. They serve different purposes and work best together.
| Aspect | Media Query | Container Query |
|---|---|---|
| Reference | Viewport / screen | Nearest container |
| Context | Global layout | Component-level |
| Reusability | Component depends on viewport | Component adapts to any context |
| Browser Support | Universal | Modern browsers (Chrome 105+, Safari 16+, Firefox 110+) |
| Use case | Page layout, sidebars, global breakpoints | Cards, widgets, panels, reusable components |
info
| 1 | /* Page layout — media queries */ |
| 2 | .layout { |
| 3 | display: grid; |
| 4 | grid-template-columns: 1fr; |
| 5 | } |
| 6 | |
| 7 | @media (min-width: 768px) { |
| 8 | .layout { |
| 9 | grid-template-columns: 240px 1fr; |
| 10 | } |
| 11 | } |
| 12 | |
| 13 | /* Component inside — container queries */ |
| 14 | .widget-container { |
| 15 | container: widget / inline-size; |
| 16 | } |
| 17 | |
| 18 | @container widget (min-width: 300px) { |
| 19 | .widget { flex-direction: row; } |
| 20 | } |
| 21 | |
| 22 | @container widget (max-width: 299px) { |
| 23 | .widget { flex-direction: column; } |
| 24 | } |
Here are practical patterns for building component-level responsive designs with container queries.
Responsive Card Component
| 1 | .card-container { |
| 2 | container: card / inline-size; |
| 3 | } |
| 4 | |
| 5 | @container card (min-width: 450px) { |
| 6 | .card { |
| 7 | display: grid; |
| 8 | grid-template-columns: 200px 1fr; |
| 9 | gap: 20px; |
| 10 | } |
| 11 | .card-image { |
| 12 | aspect-ratio: auto; |
| 13 | height: 100%; |
| 14 | } |
| 15 | .card-cta { |
| 16 | align-self: end; |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | @container card (max-width: 449px) { |
| 21 | .card { |
| 22 | display: flex; |
| 23 | flex-direction: column; |
| 24 | } |
| 25 | .card-image { |
| 26 | aspect-ratio: 16 / 9; |
| 27 | width: 100%; |
| 28 | } |
| 29 | .card-cta { |
| 30 | width: 100%; |
| 31 | } |
| 32 | } |
Dashboard Widget Grid
| 1 | .dashboard { |
| 2 | container: dashboard / inline-size; |
| 3 | } |
| 4 | |
| 5 | @container dashboard (min-width: 900px) { |
| 6 | .widget-grid { |
| 7 | display: grid; |
| 8 | grid-template-columns: repeat(3, 1fr); |
| 9 | gap: 16px; |
| 10 | } |
| 11 | } |
| 12 | |
| 13 | @container dashboard (600px <= inline-size < 900px) { |
| 14 | .widget-grid { |
| 15 | display: grid; |
| 16 | grid-template-columns: repeat(2, 1fr); |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | @container dashboard (max-width: 599px) { |
| 21 | .widget-grid { |
| 22 | display: flex; |
| 23 | flex-direction: column; |
| 24 | } |
| 25 | } |
Navigation with Container Queries
| 1 | .nav-container { |
| 2 | container: nav / inline-size; |
| 3 | } |
| 4 | |
| 5 | @container nav (min-width: 600px) { |
| 6 | .nav-list { |
| 7 | display: flex; |
| 8 | gap: 24px; |
| 9 | } |
| 10 | .nav-toggle { display: none; } |
| 11 | } |
| 12 | |
| 13 | @container nav (max-width: 599px) { |
| 14 | .nav-list { |
| 15 | display: none; |
| 16 | } |
| 17 | .nav-toggle { |
| 18 | display: block; |
| 19 | } |
| 20 | } |
Container queries resolve to the nearest ancestor with container-type. When containers are nested, a @container rule without a name queries the closest container ancestor.
| 1 | .outer { |
| 2 | container-type: inline-size; |
| 3 | container-name: outer; |
| 4 | } |
| 5 | |
| 6 | .inner { |
| 7 | container-type: inline-size; |
| 8 | container-name: inner; |
| 9 | } |
| 10 | |
| 11 | /* This queries the nearest container (inner) */ |
| 12 | @container (min-width: 300px) { |
| 13 | .child { color: #00FF41; } |
| 14 | } |
| 15 | |
| 16 | /* This queries the outer container specifically */ |
| 17 | @container outer (min-width: 600px) { |
| 18 | .child { font-size: 1.5rem; } |
| 19 | } |
| 20 | |
| 21 | /* Style queries — not yet widely supported */ |
| 22 | @container inner (style(--variant: compact)) { |
| 23 | .child { gap: 4px; } |
| 24 | } |
note
Container queries are supported in all major browsers as of 2024. The feature is stable and production-ready.
| Browser | Support | Version |
|---|---|---|
| Chrome | ✓ | 105+ |
| Edge | ✓ | 105+ |
| Firefox | ✓ | 110+ |
| Safari | ✓ | 16+ |