WSS
BlogArchitectureCSSPublished

Container Queries: Why the Component, Not the Viewport, Should Decide

A media query breakpoint answers a question a component doesn't need answered: how wide is the browser window. A component that appears in a sidebar and a full-width hero needs to know its own width instead - that's what container queries measure, and they're Baseline-supported now.

A @media breakpoint answers one question: how wide is the browser viewport. That question is the wrong one for most component-level layout decisions, and the mismatch shows up the moment a component is reused somewhere other than where it was designed.

Where viewport breakpoints fail, concretely

A card component, designed full-width on a content page, rearranges to a horizontal layout above 768px and stacks below it - reasonable, tested, ships fine.

Reuse that same card inside a 320px sidebar on a 1440px desktop viewport. The media query sees 1440px, applies the “wide” horizontal layout, and the card breaks inside its 320px container - because the query measured the window, not the box the card is actually sitting in. The component has no way to know, from a @media query alone, how much space it’s actually been given.

This isn’t an edge case. It’s every design system’s grid, sidebar-widget, and dashboard-tile scenario, and it’s the specific problem @container exists to solve.

The query, correctly scoped

.card-region {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 30rem) {
  .card {
    grid-template-columns: 10rem 1fr;
    grid-template-areas: "media body";
  }
}

.card {
  display: grid;
  grid-template-areas: "media" "body";
}

The same .card component now rearranges based on the width of .card-region - wherever that region happens to sit, sidebar or hero, without a single line of viewport-specific code. Put the identical markup in a 320px sidebar and a 1200px hero and each queries its own container correctly.

container-type: inline-size establishes containment on the inline axis only - the dimension @container queries against by default. This is deliberate: containing only the axis you’re querying avoids the layout cost of full containment on axes the component doesn’t need constrained.

Container query units

Once an ancestor establishes a container, descendants can size relative to that container rather than the viewport:

.card__title {
  /* 1% of the container's inline size, not the viewport's */
  font-size: clamp(1.125rem, 0.9rem + 2cqi, 1.75rem);
}
UnitMeasures
cqw / cqh1% of container width / height
cqi / cqb1% of container inline / block size - logical, writing-mode aware
cqmin / cqmaxSmaller / larger of the inline and block equivalents

Prefer cqi over cqw by default. It’s the logical-axis equivalent and stays correct in vertical writing modes and RTL layouts, where cqw doesn’t adjust.

The failure mode to know before you debug it for an hour: container units silently fall back to resolving against the small viewport if no ancestor has established containment. No error, no warning - a cqi value just behaves like a vw value, and the bug looks like a typo rather than a missing container-type declaration three levels up the tree.

Naming containers for unambiguous nesting

Unnamed containers query the nearest ancestor with container-type set - fine for simple trees, ambiguous once containers nest:

.page-layout    { container-type: inline-size; container-name: page; }
.sidebar-widget { container-type: inline-size; container-name: widget; }

@container page (min-width: 60rem) { /* the outer container */ }
@container widget (min-width: 20rem) { /* the inner one, explicitly */ }

Named containers remove ambiguity about which ancestor a query is actually targeting once you have more than one containment boundary in a component’s ancestor chain - worth doing proactively in any design system with nested composable regions, rather than retrofitting once a query starts matching the wrong container.

Style queries: querying computed values, not just size

A newer capability, less widely known: @container can query a container’s custom property values, not only its dimensions.

.theme-region {
  container-type: inline-size;
  --variant: default;
}
.theme-region[data-variant="compact"] { --variant: compact; }

@container style(--variant: compact) {
  .card { padding: var(--space-xs); gap: var(--space-2xs); }
}

This lets a component adapt to a design condition set by an ancestor - a “compact” mode flag, a theme variant - using the same containment mechanism as a size query, rather than reaching for a class-based override system layered on top. Support for style queries trails size queries; verify current browser coverage before depending on it for anything load-bearing.

What container queries don’t replace

Page-level, macro layout - moving from a three-column app shell to a single-column mobile layout - is legitimately a viewport concern. The overall page structure responds to how much screen the user has, not to a component’s own width, because at that level there is no meaningful “container” other than the viewport itself. Keep @media for structural, page-level breakpoints and @container for component-level adaptation. They’re complementary, not competing:

/* Page-level: viewport decides the shell */
@media (min-width: 60rem) {
  .app-shell { grid-template-columns: 16rem 1fr; }
}

/* Component-level: each card decides its own layout,
   correctly, wherever the shell puts it */
.card-region { container-type: inline-size; }
@container (min-width: 24rem) {
  .card { grid-template-columns: 8rem 1fr; }
}

Browser support

Container queries (container-type, @container size queries) reached Baseline widely available status in 2023 and are safe to use unconditionally in a modern support matrix as of this writing. Container query units (cqi, cqw, etc.) shipped alongside. Style queries are newer and less universally supported - check current coverage specifically for that feature before relying on it.

Frequently asked questions

Why do viewport breakpoints fail for reusable components?

A @media query measures the browser window, not the space actually allocated to a component. A component reused in a narrow sidebar on a wide viewport gets the wide-viewport layout rules regardless of how little room it actually has.

What’s the difference between cqi and cqw?

cqw is always the physical width axis. cqi is the logical inline axis, which matches cqw in a standard horizontal, left-to-right layout but correctly adapts in vertical writing modes or RTL contexts. Prefer cqi unless you have a specific reason not to.

Why is my cqi value behaving like a viewport unit?

No ancestor has container-type set. Container units fall back to the small viewport silently when there’s no established container - this is the most common debugging trap with the feature.

Do container queries replace media queries entirely?

No. Page-level, structural layout decisions - the overall shell responding to available screen space - remain a legitimate @media use case. Container queries handle component-level adaptation to the space a component has actually been given.

Are style queries production-ready?

Support trails size queries. Verify current browser coverage before depending on them for anything load-bearing; size queries and container units are the safe, universally-supported part of this feature set as of this writing.

Sources


Related posts