All articles
·
CSS Container Queries Frontend Tutorial

CSS Container Queries: The Complete Practical Guide

Everything you need to know about @container, container-type, and cq units — with real-world examples and the gotchas nobody warns you about.

Container queries landed in all major browsers in 2023 and fundamentally change how we write responsive components. Instead of responding to the viewport, elements can now respond to their container — the parent element that wraps them. This post covers everything you need to know to use them in production.

Why container queries exist

Viewport-based media queries have a core limitation: a component doesn’t know where it will be placed. A card at full width looks different from the same card in a two-column sidebar, but both exist at the same viewport width. You end up writing different CSS for each context, tightly coupling the component to its placement.

Container queries break this coupling. The card responds to the size of its container, not the viewport. Move it to a sidebar and it adapts automatically.

The setup: container-type

Before a container query can run, you must declare the element as a containment context. This is the part most developers trip over first.

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

inline-size establishes containment on the inline axis (horizontal in most languages). This is the most common option. Use size only if you also need to query the block axis — it has a stricter containment model that can cause layout surprises.

/* Query the container */
.card-wrapper {
  container-type: inline-size;
}

.card {
  display: grid;
  grid-template-columns: 1fr;
}

@container (min-width: 480px) {
  .card {
    grid-template-columns: auto 1fr;
  }
}

Named containers

When you have nested containers, you can name them and query by name to avoid ambiguity.

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

.main-content {
  container-type: inline-size;
  container-name: main;
}

/* Only reacts to sidebar, not main */
@container sidebar (max-width: 300px) {
  .nav-link span {
    display: none;
  }
}

This is especially useful in design systems where components may be nested inside other containers.

Container query units

Container queries introduce a new set of relative units that resolve against the query container:

UnitResolves to
cqw1% of container width
cqh1% of container height
cqi1% of container inline size
cqb1% of container block size
cqs1% of the smaller dimension
cql1% of the larger dimension

These let you scale values relative to the container, not the viewport:

.card-title {
  /* Scales with the card's container, not the page */
  font-size: clamp(1rem, 4cqi, 1.5rem);
}

The gotchas

A container cannot query itself. The container-type must be on the parent, not the element that changes. Querying yourself creates a circular dependency the spec explicitly forbids.

/* Wrong — querying itself */
.card {
  container-type: inline-size;
}
@container (min-width: 400px) {
  .card {
    /* this card queries its own parent, not itself */
  }
}

/* Correct */
.card-wrapper {
  container-type: inline-size;
}
@container (min-width: 400px) {
  .card {
  }
}

display: contents breaks containment. If you remove an element from the box model with display: contents, it can no longer serve as a container. The containment context collapses.

Percentage heights need container-type: size. If you want to use cqh or cqb units, you need block-size containment — which requires container-type: size, not just inline-size. Block containment requires the element to have a defined height, which limits its use to specific layout contexts.

container-type implies overflow: clip on the containment axis. Content clipping behavior changes when containment is applied. This rarely causes visible issues, but be aware when using transformed children or negative margins.

Combining with :has()

Container queries and :has() are two of the most powerful recent additions to CSS, and they compose well:

/* Change layout when a card contains an image */
@container (min-width: 480px) {
  .card:has(img) {
    grid-template-columns: 200px 1fr;
  }
}

Browser support

All major browsers have supported container queries since Chrome 105, Safari 16, and Firefox 110. As of 2025, global support is above 90%. For the remaining percentage, standard fallback CSS (mobile-first, single-column) is a clean degradation strategy.

Practical starting point

Here’s a portable pattern I use as a starting point for responsive cards:

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

.card {
  padding: 1rem;
  border-radius: 8px;
  background: var(--surface);
}

/* Compact: single column, tight */
.card-content {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* Medium: side-by-side image + text */
@container card (min-width: 400px) {
  .card-content {
    flex-direction: row;
    align-items: start;
  }
}

/* Wide: three-column grid */
@container card (min-width: 640px) {
  .card-content {
    display: grid;
    grid-template-columns: 180px 1fr auto;
    align-items: center;
  }
}

This card works correctly whether it’s inside a sidebar, a modal, or the main content area — no per-context overrides needed.


If you want to see container queries in action and explore the units interactively, check out CQ Playground — a tool I built specifically for this.