Overview

Media queries have always had a fundamental problem: they tell you the size of the viewport, but components don't live in the viewport — they live in a container, which might be a sidebar, a modal, a grid cell, or a card. A component that's narrow in one place and wide in another can't be styled correctly with media queries alone.

Container queries fix this. It took a decade of proposals and browser disagreements, but they shipped, and they change how you write responsive components.

The basic idea

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

@container card (min-width: 400px) {
    .card {
        display: grid;
        grid-template-columns: 200px 1fr;
    }
}

@container card (max-width: 399px) {
    .card {
        display: block;
    }
}

Three pieces:

  1. container-type: inline-size on the parent declares it as a query container, tracking its inline size (width in horizontal writing modes).
  2. container-name: card gives it a name so queries can target this specific container. Optional, but recommended once you have nested containers.
  3. @container rules apply styles when the container meets a condition.

The card now looks different based on how wide its container is, not how wide the viewport is. Put it in a sidebar and it stacks. Put it in the main content area and it goes side-by-side. Same component, same CSS.

Why media queries couldn't do this

ApproachLimitation
Media queriesOnly know the viewport, not the container
Element queries (never shipped)Created circular dependencies; a component's size could depend on its own content
Container queriesSize of the container, not the element — no circularity

The key insight that made this implementable: query the container, not the element. The container's size doesn't depend on the element's contents, so there's no cycle.

The syntax you'll actually write

Named vs unnamed containers

/* Unnamed — queries the nearest ancestor container */
@container (min-width: 400px) { ... }

/* Named — queries the nearest ancestor named "sidebar" */
@container sidebar (min-width: 400px) { ... }

Always name your containers when nesting. An unnamed query walks up to the nearest container ancestor, which might not be the one you intended.

Container types

ValueEffect
normalDefault; not a query container
inline-sizeQueries width (horizontal writing modes)
sizeQueries width and height — requires the container's size to not depend on its contents

inline-size is what you want for nearly every case. size requires the container to have an explicit height, which means a card whose height depends on its content can't use it. This was the source of most of the "why doesn't this work" moments I had early on.

Multiple conditions

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

Container query units

Along with container queries, browsers added units based on the container's size:

UnitRelative to
cqw1% of container width
cqh1% of container height
cqi1% of container inline size
cqb1% of container block size
cqmin, cqmaxThe smaller / larger of the two

These are like vw and vh, but relative to the container instead of the viewport. Fluid typography inside a card becomes trivial:

.card-title {
    font-size: clamp(1rem, 4cqi, 2rem);
}

That font size scales with the container's width. In a narrow sidebar it stays small; in a wide modal it grows, and never exceeds 2rem.

A real component

Here's a card component that adapts from stacked to horizontal layout without any JavaScript and without knowing anything about the page it's embedded in:

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

.card {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1rem;
    border: 1px solid var(--border);
    border-radius: 8px;
}

.card__image {
    aspect-ratio: 16 / 9;
    width: 100%;
    object-fit: cover;
    border-radius: 4px;
}

@container card (min-width: 500px) {
    .card {
        flex-direction: row;
        align-items: center;
    }

    .card__image {
        aspect-ratio: 1 / 1;
        width: 150px;
        flex-shrink: 0;
    }

    .card__title {
        font-size: clamp(1.25rem, 3cqi, 1.75rem);
    }
}

@container card (min-width: 800px) {
    .card {
        padding: 2rem;
        gap: 2rem;
    }

    .card__image {
        width: 220px;
    }
}

Drop it into a 300px sidebar and it stacks vertically. Drop it into a 900px content area and it becomes a horizontal card with a square image. Same markup, same CSS file.

This is the thing media queries fundamentally can't do. With media queries you'd need either a "compact mode" variant triggered by a class, or JavaScript measuring the container, or accepting that the component looks wrong in one of the two contexts.

Where they don't replace media queries

Container queries are about components. Media queries are about the page. You still need both.

Use media queries forUse container queries for
Page-level layout: is there a sidebar?Does this component have room for two columns?
Responsive grid tracksCard internals
Navigation: hamburger vs horizontalDropdown menus that reposition
Print stylesAnything reused in multiple contexts
User preferences: dark mode, reduced motionComponent variants

A common mistake is trying to do everything with container queries. The page-level layout still needs media queries, because the page itself has no container above it to query.

Browser support and the fallback story

Container queries are supported in all current browsers — Chrome 105, Safari 16, Firefox 110, and everything since. Container query units came slightly later (Chrome 105, Safari 16, Firefox 110 as well, roughly).

For an app targeting modern browsers, no fallback is needed. For something with a long tail of older devices, you can write media query fallbacks alongside:

/* Fallback for old browsers */
@media (min-width: 700px) {
    .card { flex-direction: row; }
}

/* Container query takes precedence where supported */
@container card (min-width: 500px) {
    .card { flex-direction: row; }
}

The container query rule wins wherever it's supported, and the media query catches everything else. It's not perfect — the breakpoints won't match the actual container widths — but it's a reasonable degradation.

You can also feature-detect in JS:

if (CSS.supports("container-type: inline-size")) {
    document.documentElement.classList.add("cq-supported");
}

The mental shift

The thing that took me a while to internalize: container queries let you write components that are genuinely context-independent. Before, every reusable component needed to know something about where it was being used — either a modifier class, a prop, or a JS measurement. Now it can style itself based on the space it's given.

This sounds small. In practice, it eliminates a whole category of "the component looks broken in this one place" bugs, and it removes the temptation to reach for JavaScript when CSS can do the job.

If you maintain a component library, this is the feature that's most worth adopting first. Media queries in component CSS were always a lie — the component doesn't care about the viewport. Now you can stop pretending they do.