Rendering PipelineApr 19, 2026·6 min read·

Container queries — the layout primitive CSS spent 15 years missing

Media queries ask 'how big is the viewport?'. That's the wrong question for a component. Container queries ask 'how big is the slot this component was dropped into?' — which is the question any reusable component has always needed to answer. Here's the spec, the four `container-type` values, and the migration that replaces every sidebar-vs-main-content branch.

Same card, three container widths. No media queries — the breakpoints live on the parent:

narrow · 220px
Notification settings
Control how alerts reach you
medium · 420px
Notification settings
Control how alerts reach you
wide · 640px
Notification settings
Control how alerts reach you
drag to resize the container
380px
Notification settings
Control how alerts reach you
Breakpoints are on the CONTAINER, not the viewport. The card adapts based on its parent's width — the same card rendered in a sidebar behaves differently from the same card in a full-width section.
.card-container {
  container-type: inline-size;  /* opt this parent into container queries */
  container-name: card;         /* optional — lets queries target by name */
}

@container card (min-width: 320px) {
  .card { display: flex; align-items: center; gap: 0.75rem; }
}

@container card (min-width: 540px) {
  .card .action { display: inline-flex; }
}

For 15 years, responsive design meant "query the viewport, then hope the component lives in a slot that matches." A product card at the top of a full-width hero was one layout. The same card in a sidebar was a different component — or the same component with a stack of media-query-driven overrides keyed off some .sidebar .card escape hatch. Every design system had this problem. None of them solved it cleanly until CSS container queries shipped in 2023.

Container queries ask "how big is my parent?", not "how big is the viewport?" That's the question a component has always needed to answer. CSS finally has syntax for it.

tl;dr

Declare a parent as a query container with container-type: inline-size (or size, or scroll-state). Write breakpoints against that container with @container (min-width: 320px) { … }. The component adapts to its slot, not the viewport. Supported in all evergreen browsers since Feb 2023 (Chrome 106 / Firefox 110 / Safari 16). Cost: the container must accept inline-size containment (or full size containment for container-type: size). Use inline-size by default; only reach for size when you need height queries.

What the spec says

Container queries are defined in CSS Conditional Rules Module Level 5 (§5). They were originally drafted in CSS Containment Level 3 and later moved — the Conditional Rules draft is the current normative home.

A container query is a conditional group rule that applies styles based on the features of a query container. Query containers are established by the container-type property on their principal box.

Two ideas bundled together:

  1. Query container — any element with a non-default container-type becomes a target for @container rules.
  2. Conditional group rule — the queries use the same grammar as @media, just with "container" semantics replacing "viewport" semantics.

The default is container-type: normal — elements do NOT become query containers unless you opt in. That's deliberate. Inline-size containment has a cost: the container's inline dimension can no longer depend on its descendants. The spec chose "explicit opt-in" over "pay the cost everywhere".

The four container-type values

container-type: inline-size
enables · Queries on `inline-size` (width in horizontal writing modes) work against this container.
cost · Applies inline-size containment: the container's inline dimension is established without consulting descendants.
@container (min-width: 320px) {
  .card { flex-direction: row; }
}
Style queries (@container style(--theme: dark)) are also spec'd — they let you query a container's custom-property values. Baseline support landed in Chrome 111 / Safari 18; check caniuse before relying on them.

The rule of thumb:

  • inline-size — default choice. You can query width (the inline dimension); the container applies inline-size containment. Works for 95% of component responsiveness needs.
  • size — you can query both width and height. Requires the container to have explicit dimensions; a size-contained box with no declared height collapses to 0.
  • scroll-state — Chrome / Edge 133+ (Jan 2025); not yet in Firefox or Safari. Query sticky-stuck state and scroll position. Less invasive than size; pairs with position: sticky for self-styling sticky headers.
  • normal — the default; NOT a query container.

The common mistake: setting container-type: size on a wrapper with no explicit height. The wrapper vanishes. Every developer spends 20 minutes figuring out why. The fix is usually "I meant inline-size".

The migration pattern

Before container queries, a responsive product card looked like this:

/* Viewport-based — works for the page layout, fails for components */
.card { display: flex; flex-direction: column; }
 
@media (min-width: 768px) {
  .card { flex-direction: row; }
}
 
/* Escape hatch for when the card is in a sidebar —
   viewport is the only lever you have */
@media (min-width: 768px) and (max-width: 1100px) {
  .sidebar .card { flex-direction: column; }
}

Two problems:

  1. The component's behaviour depends on who its parent is. Breaking encapsulation.
  2. The same component can't live in a 320px slot on a 1440px viewport.

With container queries:

.card-slot {
  container-type: inline-size;
  container-name: card;
}
 
.card {
  display: flex;
  flex-direction: column;
}
 
@container card (min-width: 320px) {
  .card { flex-direction: row; }
}

Now the card adapts to its slot. Full-width hero? Horizontal. Sidebar? Column. Grid cell at 400px? Horizontal. No escape hatches. The component is self-contained.

The cqi / cqw units

Container queries ship with their own unit family — viewport-unit-like but relative to the container:

UnitMeaning
cqw1% of the query container's width
cqh1% of the query container's height
cqi1% of the query container's inline size
cqb1% of the query container's block size
cqminsmaller of cqi and cqb
cqmaxlarger of cqi and cqb

Useful for fluid typography scoped to a component:

.card-slot { container-type: inline-size; }
.card-title {
  font-size: clamp(1rem, 4cqi, 1.5rem);
}

Title font scales from 1rem at narrow containers to 1.5rem at wide ones, regardless of viewport. Perfect for components that live at multiple container widths.

When to stick with @media

Container queries are the right answer for components. They are not a replacement for media queries, which still own two things:

  1. Page-level layout — "sidebar on desktop, stacked on mobile" is a viewport question. Use @media.
  2. User preferences@media (prefers-reduced-motion), @media (prefers-color-scheme: dark), @media (prefers-reduced-data). These live on the user's environment, not any container.

The rule of thumb: if the component's behaviour depends on the component's slot size, use @container. If it depends on the user's viewport or preferences, use @media.

Browser support + shipping

All evergreen browsers since Feb 2023:

  • Chrome / Edge 106 (Sep 2022)
  • Firefox 110 (Feb 2023)
  • Safari 16 (Sep 2022)

container-type: scroll-state is newer — Chrome / Edge 133+ (Jan 2025); not yet supported in Firefox or Safari. Check caniuse if relying on it.

No polyfill is needed for the common case; older browsers degrade to the un-queried base styles. As long as your "no-query" layout is usable, the degradation is graceful.

Primary sources