cd ../writing
// css · component architecture

Container queries in production — the architectural shift.

Container queries hit universal browser support in 2023 and most tutorials still show the same useless example: a card that gets two columns at 400px. That misses the point. Container queries don't just give you a new media query — they enable a fundamentally different way of designing components. Here's what changes when you internalize the shift, with production patterns and the things to watch out for.

universal browser support 5 patterns component-first CSS © use anywhere

01The problem with media queries

Media queries answer one question: "How big is the viewport?" That's been the only question CSS could ask about layout context for 15 years. It worked when responsive design meant "phone vs tablet vs desktop." It stopped working the moment we started shipping the same component into a sidebar, a modal, a hero section, and a full-page grid.

A card component might be 1000px wide on the homepage hero, 320px wide in the sidebar, and 600px wide in the detail modal — all on the same desktop viewport. Media queries can't tell you any of that. They only know "this is desktop." So your card has to either pick one layout for the whole desktop range, or rely on JavaScript to measure its actual size and apply classes.

Container queries answer the question we actually wanted to ask: "How big is the box I'm inside?" Same component, three different rendering contexts, three different layouts — without measuring anything in JavaScript.

02The two-line setup

Any element can become a "container." You opt in with one property:

✓ enabling container queries
/* 1. Declare a container */
.card {
  container-type: inline-size;   /* watch width changes */
  container-name: card;          /* optional but recommended */
}

/* 2. Query the container from inside */
@container card (min-width: 400px) {
  .card-grid { grid-template-columns: 1fr 1fr; }
}

That's the whole API. container-type: inline-size tells the browser "this element's width matters for child layout." Descendants can now ask @container card (min-width: 400px) and the rule fires based on the container's actual width — regardless of viewport size, regardless of where the card is placed in the page.

Why container-name: if you nest containers, queries match the closest matching ancestor. Naming them avoids ambiguity. Use a name even if you don't think you need it — future you will.

03The honest card recipe

Every container query tutorial uses a card. Let me write one that handles three real cases: stacked on small, side-by-side on medium, full hero layout on large — all in the same component.

✓ three-mode card
.card {
  container-type: inline-size;
  container-name: card;
}

/* Small: stacked, padded */
.card-inner {
  display: grid;
  gap: 12px;
  padding: 16px;
}
.card .thumb { aspect-ratio: 16/9; }

/* Medium: side-by-side */
@container card (min-width: 420px) {
  .card-inner {
    grid-template-columns: 140px 1fr;
    padding: 20px;
  }
  .card .thumb { aspect-ratio: 1; }
}

/* Large: hero layout with body */
@container card (min-width: 720px) {
  .card-inner {
    grid-template-columns: 1fr 1fr;
    padding: 32px;
    gap: 28px;
  }
  .card .body { display: block; }     /* show body only at large */
}

Now you can drop this card into a sidebar, a modal, a hero section, or a grid of 4 — and each instance picks the right layout based on its actual width. Same markup. Same CSS. Different appearance per context, decided by the browser.

04Container query units — the underrated half

Container queries also gave us new length units: cqw, cqh, cqi, cqb, cqmin, cqmax. They work like vw/vh but reference the container instead of the viewport.

This is enormously useful for typography that scales within its container — like a card title that's always 8% of card width, regardless of where the card lives.

✓ container-relative typography
.card {
  container-type: inline-size;
}
.card h2 {
  font-size: clamp(18px, 7cqi, 32px);
  /* 7% of container inline-size, clamped to 18-32px */
}

Combined with clamp(), you get typography that scales smoothly with the container, with min/max guards to prevent absurd sizes. No more breakpoint cascades for type sizes — one rule covers every context the component might be placed in.

05Container style queries — the new toy

Style queries (@container style(...)) let you target descendants based on custom property values on the container. The use case: themed components that adapt to context without explicit classes.

✓ style-based theming
.section {
  container-type: inline-size;
  --theme: light;
}
.section.dark { --theme: dark; }

@container style(--theme: dark) {
  .card { background: var(--ink); color: var(--bg); }
}
@container style(--theme: light) {
  .card { background: white; color: var(--ink); }
}

Style queries are Chrome-only as of mid-2026 (Safari has implementation but not shipped). Treat them as progressive enhancement for now.

06The gotchas you only learn by shipping

Containers create a containing block for absolute positioning. An absolute child inside a container element will position itself against the container, not the viewport. If you have tooltips, modals, or popovers that were positioning correctly before, they'll suddenly snap inside the card.

You can't query the container element itself. The element with container-type is the container. Queries apply to descendants. If you need to style the container based on its own size, wrap it in a parent container.

container-type: size requires explicit height. size watches both width and height. But to query height, the container needs a defined height — content-based heights cause circular layout dependencies. The browser will silently fall back. Use inline-size unless you actually need height queries, which is rare.

Container queries don't propagate through Shadow DOM boundaries by default. If you're shipping web components, each Shadow root needs its own container declaration. The outer container won't be visible from inside.

Performance cost is real but small. Each container is an additional layout observer for the browser. Hundreds of containers on a page (a long list of cards, for example) add up. Profile if you suspect issues; in practice, the cost is rarely noticeable.

07The architectural shift

The real story isn't the queries. It's what they enable: truly self-contained components. Before container queries, every component's responsive behavior was tied to viewport breakpoints chosen at the page level. Components were never really portable — they came with implicit assumptions about the layout they'd be placed in.

With container queries, a component carries its own breakpoints. You can move it to any context — sidebar, modal, hero, list, embed — and it does the right thing without modification. This is the actual long-promised "responsive components" model. Tailwind never delivered it. CSS-in-JS never delivered it. The browser delivered it.

Design systems benefit most. A Button, Card, or Form component shipped to multiple teams now responds correctly in any of those teams' layouts. The component library's CSS contract is "I am responsive to my container, not to the page." That's a much cleaner abstraction than the one we shipped to npm for ten years.

What this is really about

Container queries close a loop that's been open since the original responsive design article in 2010. Responsive design said: "components should adapt." But CSS only let them adapt to one thing — the page. Now they adapt to themselves.

That sounds small. It isn't. It's the difference between writing CSS for a layout and writing CSS for a component. The mental shift is the same shift that components originally promised. We finally have the rendering primitive to match.