Chapter 2 — Container Queries
Media queries let components react to the viewport. Container queries let
them react to their own parent's size. The distinction sounds small — it
changes everything about how you write component CSS. A card that adapts based on
whether it's in a narrow sidebar or a wide main column no longer needs two separate
media query breakpoints; it adapts based on the space it actually has. This chapter
covers the full containment model, size queries, style queries, container query units,
nested queries, and every edge case you'll encounter building real components.
Baseline 2023. Container queries (@container),
size containment (container-type: inline-size), and container query
units (cqi, cqb, etc.) are supported in all evergreen
browsers. Style queries are also supported in all evergreens as of 2024, but only
for custom properties — not yet for standard properties like color.
1. The Containment Model
Container queries build on CSS containment — the contain
property. Containment tells the browser that an element's rendering is isolated from the
rest of the page, enabling optimisations. container-type applies a specific
flavour of containment automatically.
size containment
contain: size
Element's size is independent of its content. The element behaves as if its
children don't exist for sizing purposes. Applied by
container-type: size. Rarely used alone — mostly in
container-type: size where you need both axes queryable.
inline-size containment
contain: inline-size
Same as size containment but only on the inline axis (width in horizontal
writing modes). Content can still affect block size. Applied by
container-type: inline-size. The most useful type — use this
90% of the time.
layout containment
contain: layout
The element's internal layout is isolated — absolutely-positioned children
don't escape it, counters are isolated, and the element creates a new
stacking context. Applied implicitly by all container-type values.
style containment
contain: style
CSS counters and quotes are scoped to the element. Separate from the
container-type: style that enables style queries — these are
different concepts sharing confusingly similar names.
/* What container-type actually does under the hood */
container-type: inline-size;
/* Is roughly equivalent to: */
contain: layout inline-size style;
container-type: size;
/* Is roughly equivalent to: */
contain: layout size style;
container-type: normal; /* default — no containment, no queryability */
container-type: scroll-state; /* draft spec — enables scroll-state queries */
container-type values
| Value | Containment applied | Width queryable | Height queryable | Use case |
| inline-size |
layout, inline-size, style |
Yes |
No |
Cards, sidebars, any width-responsive component. Default choice. |
| size |
layout, size, style |
Yes |
Yes |
Elements with explicit heights where you also need height queries. Rare. |
| normal |
none |
No |
No |
Default. Can still receive style queries if a custom property is set. |
| style |
style only |
No |
No |
Style queries only — no size queries. See Section 5. |
2. Syntax — container-type and container-name
/* Longhand */
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
/* Shorthand: container: <name> / <type> */
.sidebar {
container: sidebar / inline-size;
}
/* Multiple names on one container */
.card-wrapper {
container: card layout-region / inline-size;
/* can be queried as 'card' or as 'layout-region' */
}
/* @container syntax */
@container (min-width: 400px) { /* unnamed — matches nearest named ancestor */
.card { flex-direction: row; }
}
@container sidebar (min-width: 300px) { /* named — only matches container named 'sidebar' */
.nav-item { display: flex; }
}
/* Logical operators */
@container card (min-width: 400px) and (max-width: 700px) {
.card__media { width: 40%; }
}
@container (min-width: 400px) or (min-height: 600px) {
.card { padding: 2rem; }
}
@container not (min-width: 400px) {
.card { flex-direction: column; }
}
Size features available in @container
/* All queryable size features */
/* (container-type: inline-size only unlocks width/inline-size) */
@container (width: 400px) {} /* exact width */
@container (min-width: 400px) {} /* width >= 400px */
@container (max-width: 700px) {} /* width <= 700px */
@container (inline-size: 400px) {} /* logical equivalent of width */
/* Range syntax (same as modern media queries) */
@container (400px <= width < 800px) {} /* modern range */
@container (width >= 500px) {} /* alternative range syntax */
/* Height queries — only if container-type: size (not inline-size) */
@container (height: 300px) {}
@container (block-size: 300px) {} /* logical block */
@container (aspect-ratio: 16 / 9) {} /* ratio — requires container-type: size */
@container (orientation: landscape) {} /* derived from aspect-ratio */
3. Live Demo — Resizable Card
Drag the right edge of the box below. The card adapts its layout at 380 px and
560 px of its container's width — not the viewport.
Live — resize me
[ img ]
Container-Adaptive Card
This card switches from a stacked layout to a
horizontal grid at 380 px of its container width, then scales
padding at 560 px. Resize the outer box to see it adapt.
@container
Resize handle appears at the right edge of the dashed box.
/* The CSS behind the demo above */
.card-wrapper {
container: card / inline-size;
}
.card {
display: grid;
grid-template-columns: 1fr; /* default: stacked */
gap: 10px;
padding: 14px;
}
@container card (min-width: 380px) {
.card { grid-template-columns: 120px 1fr; }
}
@container card (min-width: 560px) {
.card { padding: 20px; gap: 16px; }
}
4. Container Query Units
Container query units (cq units) are length values relative to the queried
container — not the viewport. They let you size things proportionally to the
container without needing a query breakpoint at all.
nearest size/inline-size container
← inline axis (cqw / cqi) →
↕ block axis (cqh / cqb)
cqw
1% of container width
cqh
1% of container height (needs size)
cqi
1% of container inline size
cqb
1% of container block size (needs size)
cqmin
1% of smaller axis
cqmax
1% of larger axis
/* cqw / cqi — use inside any descendant of a sized container */
/* Fluid type scale locked to the card container, not the viewport */
.card-wrapper {
container-type: inline-size;
}
.card__title {
font-size: clamp(1rem, 4cqi + 0.5rem, 2rem);
/* grows from 1rem to 2rem as the container widens */
}
.card__image {
width: 30cqi; /* always 30% of the card's width */
}
/* cq units in calc() */
.card__gap {
gap: calc(2cqi + 8px);
}
/* cq units vs vw units — the key difference */
.hero-text {
font-size: 5vw; /* relative to viewport — breaks in narrow sidebars */
font-size: 5cqi; /* relative to container — correct in any context */
}
/* cqh / cqb — require container-type: size (both axes contained) */
.modal-content {
container: modal / size; /* must have explicit height on .modal-content */
}
.modal-body {
max-height: 80cqb; /* 80% of the modal's block (height) */
}
/* Nearest container resolution — cq units always resolve against */
/* the nearest ANCESTOR with appropriate containment, not the root */
/* This makes them genuinely component-scoped, not global */
cqh and cqb require an explicit height on the container.
With container-type: inline-size, the block axis is not contained —
the element still grows with its content and has no queryable block size. You need
container-type: size and an explicit height (or
block-size) on the container element for these units to resolve.
5. Style Queries
Style queries let you query the computed value of a CSS custom property
on an ancestor container, rather than its size. They are available in all evergreen
browsers as of 2024 — but only for custom properties, not for standard properties
like color or display (that part of the spec is not yet
implemented).
/* Style query syntax — query a custom property on the container */
.theme-wrapper {
container-type: style; /* or 'inline-size' — style queries work on any container-type */
container-name: theme;
}
/* Apply a theme variant via a custom property on the container */
.theme-wrapper[data-theme="dark"] {
--theme: dark;
}
.theme-wrapper[data-theme="light"] {
--theme: light;
}
/* Query that custom property from any descendant */
@container theme style(--theme: dark) {
.card {
background: #161b22;
color: #c9d1d9;
border: 1px solid #30363d;
}
}
@container theme style(--theme: light) {
.card {
background: #fff;
color: #24292f;
border: 1px solid #d0d7de;
}
}
Why style queries matter
Before style queries
A card component that needs
dark-theme styles inside a
dark wrapper requires:
.dark-parent .card { ... }
This is a context-coupled
selector — the card component
must know about its parent.
With style queries
The card queries its container
for --theme: dark itself.
@container style(--theme: dark)
{ .card { ... } }
The card is self-contained —
it adapts without needing
knowledge of the parent class.
/* Advanced: combining style + size queries */
@container card style(--variant: featured) {
.card { border: 2px solid var(--accent); }
}
@container card (min-width: 400px) and style(--variant: featured) {
.card { grid-template-columns: 1fr 1fr; }
/* only wide AND featured cards get the two-column layout */
}
/* Checking for a property BEING SET (any value) */
/* Not yet standardised — avoid for now */
/* The safe pattern: use a boolean-like custom property */
.wrapper { --is-featured: true; }
@container style(--is-featured: true) {
.card { border-color: gold; }
}
6. The Containment Trap
A container cannot query itself
When you set container-type: inline-size on an element, that element
is the container. The @container rules inside it only affect its
descendants — they cannot change properties on the container
element itself. Trying to style the container element from within its own
@container query has no effect.
The fix: wrap it.
Add a wrapper element one level up. Apply container-type to the
wrapper, then write container queries that target the inner element you actually
want to style.
/* Wrong — the container cannot style itself */
.card {
container: card / inline-size;
padding: 1rem;
}
@container card (min-width: 400px) {
.card { padding: 2rem; } /* has no effect */
}
/* ─────────────────────────────────────────────────────────────── */
/* Correct — the WRAPPER is the container, the card is the target */
.card-wrapper {
container: card / inline-size;
}
.card {
padding: 1rem;
}
@container card (min-width: 400px) {
.card { padding: 2rem; } /* works */
}
/* In practice, many grid and flex wrappers are already structural */
/* elements — the wrapper rarely needs to be a purely cosmetic div */
7. Nested Container Queries
/* Multiple nested containers — each @container resolves against */
/* its nearest NAMED ancestor matching the name, or the nearest */
/* unnamed ancestor with containment if no name specified */
.layout {
container: layout / inline-size;
}
.sidebar {
container: sidebar / inline-size;
}
.card-wrapper {
container: card / inline-size;
}
/* This query matches the 'card' container directly */
@container card (min-width: 300px) {
.card { flex-direction: row; }
}
/* This one matches the 'sidebar' ancestor */
@container sidebar (max-width: 200px) {
.card .card__title { font-size: 0.875rem; }
}
/* This one matches the outermost 'layout' container */
@container layout (min-width: 1200px) {
.sidebar { display: block; }
}
/* Nesting @container rules */
@container card (min-width: 300px) {
.card { display: grid; }
/* Inner @container query — queries the named 'layout' container */
@container layout (min-width: 900px) {
.card { grid-template-columns: 1fr 1fr; }
}
}
8. Container Queries with CSS Grid and Flexbox
/* Grid auto-fit — still viewport-based */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
/* this uses the grid container's width — already component-relative */
/* auto-fill tracks the card-grid element's own width */
}
/* BUT each card inside the grid needs its OWN container query */
/* because each card is sized by the grid, not by the viewport */
.card-grid__item {
container: card / inline-size;
}
@container card (min-width: 360px) {
.card { display: flex; }
.card__image { width: 120px; flex-shrink: 0; }
}
/* ── Sidebar + main layout — classic container query use case ─── */
.page-layout {
container: page / inline-size;
display: grid;
grid-template-columns: 1fr;
}
@container page (min-width: 768px) {
.page-layout {
grid-template-columns: 240px 1fr;
}
}
/* Wait — .page-layout is both the container AND the element being styled! */
/* This hits the containment trap. Solution: apply container-type to a
grandparent, or use a media query here since this IS a layout-level concern */
/* Better pattern: media query for page-level layout decisions */
@media (min-width: 768px) {
.page-layout { grid-template-columns: 240px 1fr; }
}
/* container queries for components WITHIN the layout */
.card-wrapper {
container: card / inline-size;
}
9. Container Queries vs Media Queries
|
Media query (@media) |
Container query (@container) |
| Query target |
The viewport (browser window) |
The nearest qualifying ancestor element |
| Use for |
Page-level layout, typography scale, breakpoints for the overall page structure |
Component-level layout — a card, sidebar widget, nav, or any repeated UI element |
| Reusability |
Component must be rebuilt at each breakpoint if context changes |
Component adapts wherever it's placed — true portability |
| Performance |
Browser matches immediately — very fast |
Requires layout pass to know container size — slightly more work but negligible in practice |
| Self-styling |
Can style any element |
Cannot style the container element itself (containment trap) |
| Height queries |
Can query viewport height freely |
Height queries require container-type: size + explicit height |
| Print/other media |
Can target print, screen, speech, etc. |
No media-type targeting — size only (or style) |
The rule of thumb. Use media queries for the page skeleton — the
overall layout grid, the sidebar appearing, the nav collapsing. Use container
queries for every repeated component inside that skeleton: cards, widgets, article
previews, form sections. If the thing being styled could appear in multiple
contexts at different widths, it's a container query candidate.
10. Container Queries and @layer
/* @container inside @layer — the query lives in that layer */
@layer components {
.card-wrapper {
container: card / inline-size;
}
@container card (min-width: 400px) {
.card { display: flex; }
}
}
/* Overriding a container query from a higher-priority layer */
@layer utils {
/* This wins over the components container query — */
/* layer priority trumps everything before specificity */
.card { display: block; }
}
/* Container queries INSIDE a layer are still subject to that layer's priority */
/* A container query in utils will beat one in components (same layer rules apply) */
/* The interplay: specificity of @container rules */
/* @container conditions don't add specificity to the contained selectors */
/* The selectors inside @container have exactly their own specificity */
@container card (min-width: 400px) {
.card { color: red; } /* specificity: 0-1-0 (just .card) */
}
/* The @container condition is NOT part of the specificity calculation */
/* This means two rules inside different @container blocks may tie on specificity */
/* and source order then decides — so always write container queries after base rules */
Chapter Summary
| Concept | Key point |
| container-type | inline-size — width queryable, height not. size — both axes queryable, requires explicit height. normal — default, no queries. style — custom property queries only. |
| container-name | Required for named queries. One element can have multiple names. Use the shorthand: container: name / type. |
| @container syntax | Unnamed queries match nearest qualifying ancestor. Named queries match the nearest ancestor with that name. Supports and, or, not, and range syntax. |
| Size features | width, min-width, max-width, inline-size (logical). height and block-size need container-type: size. aspect-ratio and orientation derived from both axes. |
| CQ units | cqw/cqi — inline axis. cqh/cqb — block axis (needs size). cqmin/cqmax — smaller/larger axis. Resolve against nearest ancestor with appropriate containment. |
| Style queries | Query computed value of a custom property on an ancestor container. Currently works for custom properties only — standard property queries not yet implemented in browsers. |
| Containment trap | A container cannot style itself via its own @container query. Wrap it: apply container-type to the parent, query from descendants. |
| Nested containers | Each @container resolves against the nearest qualifying ancestor with the matching name. Inner containers shadow outer ones of the same name. |
| CQ vs MQ | Media queries for page-level layout. Container queries for components. Mixing both is correct — use each at the right level of abstraction. |
| @container specificity | The @container condition does not add specificity. Selectors inside have their own specificity only. Write container queries after base rules to ensure source-order wins correctly. |
Exercises
- Three-state card component: Build a card that switches layout at two container breakpoints — stacked below 320 px, horizontal between 320 px and 600 px, and a featured two-column layout above 600 px. Place the card in a CSS grid with auto-fill and watch it switch layouts as the grid resizes it. Confirm that a media query alone could not achieve this correctly in multiple grid-column contexts.
- Container query fluid type: Create a component with a heading and body text. Using only
cqi units and clamp(), make the heading scale fluidly between 1 rem and 2.5 rem as the container grows from 200 px to 800 px — with no breakpoints. Confirm the type scale is locked to the container, not the viewport, by placing two instances at different column widths side by side.
- Style query theme system: Build a theme wrapper component with a
data-theme attribute and a --theme custom property. Inside it, place a button and a card. Write style queries so that both the button and card automatically adapt their colour scheme (background, border, text) based on the --theme custom property — without the button or card knowing their parent class name. Demonstrate with both data-theme="dark" and data-theme="light" wrappers on the same page.
- Named container hierarchy: Create a three-level hierarchy:
page (outermost) → sidebar → widget. Write separate @container queries for each level that change the widget's appearance. Verify that each query correctly targets its named container and not the others, then add an unnamed query and confirm it resolves against the nearest named ancestor.
- CQ + @layer interaction: Set up a layer order: base, components, utils. In the components layer, create a card container query that changes the card's flex-direction at 400 px. In the utils layer, add a rule that forces
flex-direction: column on all cards. Confirm the utils layer wins regardless of the container condition being true. Then move the utils rule inside a lower-priority layer and confirm the container query wins. Document the specificity vs layer priority interaction in a comment.
Next: Chapter 3 — Subgrid.
How grid-template-rows: subgrid and grid-template-columns: subgrid
let nested elements participate in their ancestor's grid tracks — solving the alignment
problems that were previously impossible without JavaScript. Includes cross-nested-element
alignment, subgrid and named lines, and real card-grid use cases.