w3tweaks.com · CSS Tutorial

CSS Container Queries

Components that respond to their container — not the viewport

Tab 1

Drag to Resize — Watch the Card Adapt to Its Container

This is the demo most tutorials never build: grab the bottom-right corner of the dashed container and drag. The card switches between three layouts based on the container's width — the viewport never changes.

↘ Drag the bottom-right corner of the dashed box to resize it
width: —
🎧

Wireless Headphones Pro

★★★★★ 4.8 · 2,340 reviews · Free shipping

Adaptive noise cancellation with 40-hour battery. The card layout you're seeing depends entirely on this container's width.

$249
/* 1. Declare the container */
.card-wrapper {
  container-type: inline-size;
  container-name: playground;
}

/* 2. Query the container — not the viewport */
@container playground (min-width: 400px) {
  .product-card { grid-template-columns: 130px 1fr; }
}
@container playground (min-width: 620px) {
  .product-card { grid-template-columns: 180px 1fr auto; }
}
Same Card, Same CSS — Two Contexts at Once

One component, zero extra classes. It's stacked in the sidebar and horizontal in the main area — simultaneously.

Sidebar (240px)
🎧

Wireless Headphones Pro

★★★★★ 4.8

Same CSS class — compact layout because the container is narrow.

$249
Main content (wide)
🎧

Wireless Headphones Pro

★★★★★ 4.8 · 2,340 reviews · Free shipping

Same CSS class — expanded layout because the container is wide.

$249
Tab 2

Container Query Units — Fluid Typography Inside Components

Units like cqi work like vw — but relative to the container. Drag this container: everything inside scales proportionally, from padding to font sizes.

↘ Drag the corner — typography and spacing scale with the container

Fluid inside the box

This heading is clamp(15px, 6cqi, 34px) — 6% of the container's inline size, clamped. Padding, button, and radius all use cqi too. A truly self-scaling component.

.card {
  padding: clamp(12px, 5cqi, 32px);
}
.card h3 {
  font-size: clamp(15px, 6cqi, 34px); ← 6% of CONTAINER width
}
.card .btn {
  padding: 1.5cqi 3cqi;
}
All Six Container Units
UnitMeansUse for
cqi1% of container inline size✅ Recommended default — logical, RTL-safe
cqb1% of container block sizeVertical scaling (needs container-type: size)
cqw1% of container widthPhysical width — prefer cqi instead
cqh1% of container heightPhysical height — prefer cqb instead
cqmin1% of the smaller axisSquare elements that must never overflow
cqmax1% of the larger axisBackgrounds that must always cover
Fallback gotcha: if you use cqi but no ancestor is declared as a container, the unit silently falls back to the small viewport unit (svi) — your component scales with the viewport instead of the container, and nothing errors. If cq units "aren't working," check that container-type exists somewhere up the tree.
Why cqi over cqw? cqi follows the logical inline direction — it automatically flips for vertical writing modes and stays correct in RTL layouts. cqw is physically locked to horizontal width. Same value in most Western layouts, but cqi is the future-proof habit.
Tab 3

Advanced Features & Gotchas

Named containers, style queries, and the four traps that make people say "container queries aren't working."

🏷️ Named Containers — Nesting Done Right
.page-main { container: main / inline-size; }
.card      { container: card / inline-size; }

/* Target a SPECIFIC ancestor by name */
@container main (min-width: 800px) {
  .card-grid { grid-template-columns: 1fr 1fr; }
}
@container card (min-width: 350px) {
  .card-title { font-size: 1.4rem; }
}

Without a name, @container queries the nearest ancestor container — which breaks when containers nest. Names let inner components target the outer layout container and their own wrapper independently. The container: name / type shorthand sets both at once.

🎨 Style Queries — Query Custom Properties
no --theme set
Light chip
--theme: dark
Dark chip
.section { --theme: dark; }

@container style(--theme: dark) {
  .chip { background: #0f172a; color: #e2e8f0; }
}

Set a custom property on any ancestor; descendants restyle automatically — no class on every child. Every element is a style container by default (no container-type needed). Support: Chrome, Edge, Safari 18+; Firefox still in development — treat as progressive enhancement.

⚠️ Gotcha 1: A Container Can't Style Itself
/* ❌ Trying to style the container from its own query */
.card { container-type: inline-size; }
@container (min-width: 400px) {
  .card { background: red; } ← never applies
}

/* ✅ Fix: query container styles an inner wrapper */
.card-wrapper { container-type: inline-size; }
@container (min-width: 400px) {
  .card { background: red; } ← works
}

Queries apply only to a container's descendants — never the container itself (that would create a sizing loop). The universal fix: wrap the component in a dedicated container div.

⚠️ Gotcha 2: container-type: size Collapses
/* ❌ size containment: children can't define height */
.box {
  container-type: size;
  /* no height set → collapses to 0px tall! */
}

/* ✅ Either give it explicit height... */
.box { container-type: size; height: 400px; }
/* ...or use inline-size (width only) */
.box { container-type: inline-size; }

size containment means the container's height can no longer be derived from its children — with no explicit height it collapses to zero. This is why inline-size is the recommended default: content still grows the height naturally.

🔍 "Container Queries Not Working" Checklist
1. Is container-type set on an ANCESTOR?
   (not on the element being styled)

2. Querying the container itself? → wrap it

3. Nested containers? → add container-name,
   unnamed queries hit the NEAREST container

4. Breakpoints copied from media queries?
   Containers are narrower than viewports —
   a 768px media breakpoint ≈ 400-500px container

5. Chrome DevTools: click the "container" badge
   next to the element to inspect breakpoints

Five checks resolve nearly every "not working" report. #4 is the sneaky one during migration — container breakpoint values need rethinking, not copying.

🆚 Container vs Media Queries — The Rule
@container — MICRO
✓ Cards
✓ Widgets
✓ Nav components
✓ Anything reused in multiple contexts
@media — MACRO
✓ Page skeleton
✓ Sidebar show/hide
✓ prefers-color-scheme
✓ prefers-reduced-motion
/* Fallback for legacy browsers */
@supports (container-type: inline-size) {
  /* container query enhancement */
}

If it styles the page skeleton, use @media. If it styles a component that could live anywhere, use @container. User-preference queries (color scheme, motion) are always media queries — they have no container equivalent.

What's next — scroll-state queries: the newest container query type, container-type: scroll-state, queries whether a sticky element is currently stuck, snapped, or scrollable — pure CSS for "shrink the header when it sticks." Shipped in Chrome/Edge (late 2025); guard with @supports (container-type: scroll-state) until Firefox and Safari land it.