Container queries from scratch: container-type and @container
Media queries respond to the viewport. Container queries respond to the parent. This shift lets you build truly reusable components that work in any context.
Why container queries matter
With media queries, a card component must know where it will be placed — a narrow sidebar or a wide main area — to apply the right styles. This couples the component to the page layout. Container queries decouple them: the card asks "how wide is my container?" and adapts accordingly.
/* Media query approach — coupled to the page */
@media (min-width: 768px) {
.card { flex-direction: row; }
}
/* Problem: in a 300px sidebar on a 1200px screen,
this still triggers — the card goes horizontal
even though it doesn't have room */
/* Container query approach — coupled to the parent */
@container (min-width: 400px) {
.card { flex-direction: row; }
}
/* The card only goes horizontal when its
container is at least 400px wide */
Defining a containment context
Before you can use @container, you must declare which element is the container. Use container-type to establish containment. The most common value is inline-size, which enables queries against the container's width.
/* The wrapper is the container */
.card-wrapper {
container-type: inline-size;
}
/* Three container-type values */
.inline { container-type: inline-size; } /* width queries */
.both { container-type: size; } /* width + height queries */
.normal { container-type: normal; } /* default — not a container */
/* Shorthand: container-name + container-type in one */
.sidebar {
container: sidebar / inline-size;
/* Name: sidebar, Type: inline-size */
}
container-type: inline-size establishes inline-axis containment. This means the element cannot use its children to determine its own inline size — it must get its width from its parent or an explicit value.Writing @container rules
The @container at-rule works like @media but queries the nearest ancestor container instead of the viewport. You can use min-width, max-width, and combine conditions with and, or, and not.
.card-wrapper {
container-type: inline-size;
}
/* Base: vertical card */
.card {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* When container is wide enough, go horizontal */
@container (min-width: 30rem) {
.card {
flex-direction: row;
align-items: center;
}
.card__image {
flex: 0 0 40%;
}
}
/* Even wider — show extra metadata */
@container (min-width: 50rem) {
.card__meta {
display: flex;
gap: 1rem;
}
}
Named containers
When you have nested containers, @container queries the nearest one by default. Use container-name to target a specific ancestor container by name.
.page {
container: page / inline-size;
}
.sidebar {
container: sidebar / inline-size;
}
/* Query the sidebar container specifically */
@container sidebar (max-width: 20rem) {
.widget {
font-size: 0.875rem;
padding: 0.5rem;
}
}
/* Query the page container, skipping sidebar */
@container page (min-width: 60rem) {
.widget {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
Named containers are essential in complex layouts where multiple containment contexts are nested. Without names, you would always hit the nearest container, which may not be the one you intend.
Container query units
Container queries introduce new units that resolve relative to the container's dimensions: cqw (1% of container width), cqh (1% of container height), cqi (1% of inline size), cqb (1% of block size), cqmin, and cqmax.
.card-wrapper {
container-type: inline-size;
}
.card__title {
/* Font size scales with container width */
font-size: clamp(1rem, 3cqi, 1.75rem);
}
.card__image {
/* Height relative to container */
height: 30cqi;
object-fit: cover;
}
/* cqi = container query inline, works with writing modes
cqw = always the physical width */
container-type on a parent, cqi units resolve to zero.A complete adaptive card component
Here is a full example of a card that transforms across three breakpoints based entirely on its container width — zero media queries, fully reusable in any layout context.
/* Container setup */
.card-slot {
container-type: inline-size;
}
/* Small: stacked layout */
.card {
display: grid;
gap: 0.75rem;
padding: 1rem;
border-radius: 0.5rem;
background: oklch(0.19 0.02 260);
}
.card__image {
border-radius: 0.375rem;
aspect-ratio: 16 / 9;
object-fit: cover;
width: 100%;
}
.card__actions { display: none; }
/* Medium: side-by-side */
@container (min-width: 28rem) {
.card {
grid-template-columns: 12rem 1fr;
grid-template-rows: auto 1fr;
padding: 1.25rem;
}
.card__image {
grid-row: 1 / -1;
aspect-ratio: 1;
height: 100%;
}
}
/* Large: full feature display */
@container (min-width: 45rem) {
.card {
grid-template-columns: 16rem 1fr auto;
align-items: center;
}
.card__actions {
display: flex;
gap: 0.5rem;
}
}
Drop this card into a sidebar, a main content area, or a full-width hero — it adapts to its container without any changes to the component CSS.