Vivian Voss

The Width You Never Had to Measure

css web javascript tooling

Stack Patterns ■ Episode 14

Every web developer has written this hack. A card component lives in a sidebar at 280 pixels on one page and on a dashboard at 1100 on another. You want the badges to disappear in the narrow case and the layout to switch from horizontal to vertical. You reach, rather inevitably, for JavaScript:

const observer = new ResizeObserver(entries => {
  for (const entry of entries) {
    entry.target.classList.toggle('narrow', entry.contentRect.width < 400);
  }
});

observer.observe(card);

You then write the CSS twice, once for the parent class and once again for the .narrow case. You hope the observer fires before the first paint, and most of the time it does, sometimes only after a flash of unstyled content. You move on, because the alternative is to wrap the component in a width-aware higher-order component, install a layout library, or accept the inevitable hydration mismatch in your server-side rendering pipeline.

Container queries make all of that go away. Rather quietly.

A Brief, Slightly Embarrassing History

The idea is older than most reading this article. The early 2010s saw a steady stream of element queries discussions in the front-end community, with polyfills like EQCSS and a long mailing-list debate about what the missing tool should look like. The proposals all stumbled on the same architectural problem: querying an element's own size while inside the layout pass that produces that size invites circular layout, which is roughly the equivalent of asking a function for its own return value before it has finished returning. The browser engineers, sensibly, declined to offer one.

The fix took the better part of a decade and required a quiet redefinition of the problem. The element under styling cannot query its own size; that path leads to circularity. But the element's containing context can. The work that became Container Queries, originally drafted in CSS Containment Module Level 3 and now living in CSS Conditional Rules Module Level 5, was championed by Miriam Suzanne (Invited Expert at the CSS Working Group) with contributions from many others. An element marks itself as a container and provides a stable size context to its descendants. The descendants then query that context. No circularity, no observer, no JavaScript.

Implementations followed quickly. Chrome 106 and Safari 16 shipped in September 2022; Firefox 110 followed on 14 February 2023. According to caniuse, global usage is just over 94 per cent as of March 2026. The feature has been cross-browser stable for more than three years.

Twelve Years of Quiet Patience early 2010s element queries EQCSS polyfill, debate Sep 2022 Chrome 106, Safari 16 first cross-engine ship 14 Feb 2023 Firefox 110 all three engines Mar 2026 94 per cent global usage A decade of polyfills, then three years of cross-browser stability. The ResizeObserver workaround was the world we were living in. It is not the world we live in now.

The Pattern

The simplest case takes two declarations:

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

@container (max-width: 400px) {
  .card h3   { font-size: 1rem; }
  .card .badge { display: none; }
}

container-type: inline-size tells the browser to watch the inline dimension (the writing-mode-aware horizontal axis in Latin scripts). The @container rule then matches when that dimension drops below 400 pixels. Rules inside the block apply to descendants of .card, never to .card itself.

There is also container-type: size, which watches both axes. Use it sparingly: size containment forces the container's block size to be intrinsic, which can collapse otherwise auto-height layouts in surprising ways. The inline-size form is the safe default for component-level responsiveness, and the form most CSS examples reach for.

When components nest, the closest matching ancestor wins. A query without a container-name matches the nearest ancestor with container-type set. To target a specific level, name your containers:

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

@container card (max-width: 400px) {
  /* only the card matters here, not the sidebar */
}
One Component, Two Homes Sidebar (280 px) Dashboard (1100 px) Title short subtitle body text wraps in two short lines no badges Title in larger weight subtitle continues body text fits in one wide line for clarity new featured @container Same component, same markup. The cascade chooses the layout based on the container's inline size.

The Length Units That Travel With the Container

Container queries also bring four new length units that resolve against the container rather than the viewport:

  • cqw: 1 per cent of the container's width
  • cqh: 1 per cent of the container's height
  • cqi: 1 per cent of the container's inline size (writing-mode-aware width)
  • cqb: 1 per cent of the container's block size (writing-mode-aware height)
  • cqmin and cqmax are also defined, taking the smaller or larger of cqi and cqb

Combined with clamp(), these allow typography that scales with the component, not the viewport:

h3 { font-size: clamp(1rem, 4cqi, 1.5rem); }

Drop the same component into a 280-pixel sidebar and a 1100-pixel main column, and its heading scales appropriately in both, without writing any breakpoints. The vocabulary that responsive design has wanted for fifteen years has finally arrived on the right axis.

Why It Works

The cleverness, as with @scope in the previous episode, is what container queries deliberately do not do. They do not query the element being styled; they query the size of an ancestor. The cycle that broke every previous attempt at element queries simply does not arise.

A second piece of cleverness: the container-type declaration triggers CSS containment for the chosen axes. The browser knows that nothing outside the container can affect what is inside it, and vice versa, for the purpose of size calculations. That makes the cost of container queries predictable: each container is a self-contained layout unit, evaluated once.

The performance question, which used to dominate discussions of element queries, has therefore become a much smaller question. Each container has a cost (a containment scope), but it is a known cost. One does not pay for queries one does not write.

Combined with :has()

Container queries become properly powerful in combination with :has(), the parent-aware selector covered in episode 5 of this series. A component now reacts to two independent axes at once: its own size, via @container, and its own content, via :has().

A common pattern: cards switch to a vertical layout when narrow, but only if they actually contain an image. A text-only card at the same width keeps its inline form.

.card {
  container-type: inline-size;
  display: grid;
  grid-template-columns: auto 1fr;
}

@container (max-width: 400px) {
  .card:has(img) {
    grid-template-columns: 1fr;
  }
}

The same approach scales: badges that disappear only when the container is narrow and there are more than two of them; a sidebar widget that switches layout only when it contains a form; a section heading that changes weight when it is followed by a long article. None of these require JavaScript, a class toggle, or a render hook.

Two Axes, Four Cells, No JavaScript narrow wide container size — @container image text image, narrow grid-template-columns: 1fr image, wide inline, two columns text, narrow badges hidden, smaller h3 text, wide badges visible, larger h3

The composability is the broader point. Each modern CSS feature (container queries, :has(), @scope, @layer, view transitions) is independently useful, but combinations multiply their value. The platform has spent a decade quietly assembling a vocabulary in which most former JavaScript responsibilities for layout and conditional styling become CSS again.

Honest Limitations

Three things to know before one ships container queries to production.

First, container-type: size disables auto-height. If one wants a container to react to changes in its own height (rare, but possible), the container must have a defined height rather than letting its content determine it. For most components, one only cares about width, and inline-size is the correct choice. Reach for size only when one genuinely needs both axes.

Second, style queries (the form @container style(--theme: dark) { ... }) are a separate, newer feature. As of 2026 they are supported in Chrome 111+ and Edge 111+ only; Firefox and Safari are still developing support. They allow a component to query the value of a custom property on its container, rather than its size, and are powerful for theming and design tokens. Until cross-browser parity arrives, treat them as progressive enhancement rather than a default tool.

Third, container queries do not propagate across iframe boundaries or shadow root boundaries. A widget embedded in an iframe queries its own document's containers, not the parent page's. This is generally what one wants; it is worth knowing if one builds embedded components that span those boundaries.

When to Use

Anywhere a component lives at more than one width. Cards in a sidebar and a main grid. Article previews in a related-articles strip and a featured slot. Dashboard widgets that the user can resize. Embedded widgets where one does not control the host layout. Anywhere your team currently maintains two or three classes (.card, .card--narrow, .card--wide) coordinated by JavaScript.

For a new design system, build it in containers from the start. Each component sets a container-type on its outer element and queries it from within. Page-level media queries become a layer above, handling viewport-scoped concerns: navigation collapse, hero sizing, things that genuinely depend on the device. Component-level concerns drop a level and stay there.

The Layout, Civilised

Episode 11 of this series gave native page transitions; episode 12 gave a built-in deep clone; episode 13 gave native CSS scoping. Each replaced a stack of build-time tooling and runtime libraries with a few lines of standard CSS or JavaScript. Container queries belong to the same lineage: a feature the platform has been quietly building for a decade, while the framework ecosystem invented and reinvented increasingly elaborate workarounds.

Three years on from cross-browser support, the workaround code is still in production codebases everywhere, and the platform feature still has under-used potential. If your team is still measuring component widths in JavaScript, the cascade has been waiting.

Container queries: cross-browser stable since Firefox 110 on 14 February 2023, global usage 94 per cent in March 2026. Two declarations: container-type on the parent, @container rules on the descendants. No JavaScript, no observer, no double class. cqi, cqb, cqw, cqh for typography that travels with the component. :has() for content-aware variants on the same axis. The cascade was capable all along.