Skip to content
Unverified — AI-generated content. Help verify this page

Container Queries

Container queries are the most significant CSS layout feature since Flexbox. They allow elements to respond to the size of their containing element rather than the viewport — enabling truly portable, context-aware components.

The Problem Viewport Queries Can't Solve

Consider a card component used in two contexts:

  1. Full-width (100% viewport width)
  2. In a sidebar (25% viewport width)

With viewport queries, you can't differentiate these contexts:

css
/* This media query fires at the SAME viewport width */
/* whether the card is full-width or in a sidebar */
@media (min-width: 768px) {
  .card {
    flex-direction: row; /* Wrong for cards in narrow sidebars */
  }
}

Container queries solve this:

css
/* The card now responds to ITS OWN CONTAINER width */
@container (min-width: 400px) {
  .card {
    flex-direction: row; /* Only when the card's container is wide enough */
  }
}

The same card component, in a 500px sidebar, will remain vertical. The same card at full width will switch to horizontal. No JavaScript, no extra classes, no component variants needed.

The Containment Model

To use container queries, the parent element must establish a containment context:

css
/* The container */
.card-wrapper {
  container-type: inline-size; /* Enable inline-size queries */
  container-name: card-container; /* Optional name for targeting */

  /* Shorthand */
  container: card-container / inline-size;
}

/* The component inside responds to .card-wrapper */
@container card-container (min-width: 400px) {
  .card {
    display: flex;
    flex-direction: row;
  }
}

container-type values

ValueWhat it enables
inline-sizeQueries on the container's inline dimension (usually width)
sizeQueries on BOTH inline and block dimensions
normalNo size queries, but style queries still work

container-type: size

Using size requires the container's block size (height) to be deterministic. If the container's height depends on its children's height, setting container-type: size will cause the children's height to be 0. Always prefer inline-size for typical UI components.

Container Query Units

Container queries come with new relative units:

UnitDefinition
cqw1% of container's width
cqh1% of container's height
cqi1% of container's inline size
cqb1% of container's block size
cqminSmaller of cqi or cqb
cqmaxLarger of cqi or cqb

These units work similarly to vw/vh but relative to the container:

css
.card-title {
  /* Scale font based on card width, not viewport */
  font-size: clamp(1rem, 4cqi, 1.5rem);
}

.card-image {
  /* Image is always 40% of card width */
  width: 40cqi;
}

Practical Container Query Patterns

Pattern 1: Responsive Card

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

.card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-4);
}

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

.card__content {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* When container ≥ 400px: horizontal layout */
@container (min-width: 400px) {
  .card {
    flex-direction: row;
    align-items: flex-start;
  }

  .card__image {
    width: 40%;
    aspect-ratio: 1;
    flex-shrink: 0;
  }
}

/* When container ≥ 700px: larger content */
@container (min-width: 700px) {
  .card {
    padding: var(--space-6);
    gap: var(--space-6);
  }

  .card__title {
    font-size: 1.5rem;
  }
}

Pattern 2: Navigation Component

css
.nav-container {
  container-type: inline-size;
  container-name: nav;
}

.nav {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
}

/* Compact nav for narrow containers (sidebar, drawer) */
@container nav (max-width: 200px) {
  .nav {
    flex-direction: column;
    align-items: flex-start;
  }

  .nav__label {
    display: none; /* Icon-only in very narrow contexts */
  }
}

/* Full nav for wide containers (top bar) */
@container nav (min-width: 600px) {
  .nav {
    padding: var(--space-3) var(--space-6);
    justify-content: space-between;
  }

  .nav__secondary {
    display: flex; /* Show secondary nav items */
  }
}

Pattern 3: Data Table

css
.table-container {
  container-type: inline-size;
  container-name: table;
  overflow-x: auto;
}

/* Default: horizontal scroll table */
.data-table {
  width: 100%;
  border-collapse: collapse;
}

/* Small container: card-based layout */
@container table (max-width: 600px) {
  .data-table thead { display: none; } /* Hide column headers */

  .data-table tr {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-2);
    padding: var(--space-3);
    border: 1px solid var(--color-border-default);
    border-radius: 0.5rem;
    margin-block-end: var(--space-2);
  }

  .data-table td::before {
    /* Recreate column header as data-label */
    content: attr(data-label);
    font-weight: 600;
    font-size: 0.75rem;
    color: var(--color-text-secondary);
    display: block;
    margin-block-end: var(--space-1);
  }
}
html
<!-- data-label required for CSS card layout -->
<tr>
  <td data-label="Name">John Doe</td>
  <td data-label="Email">john@example.com</td>
  <td data-label="Role">Developer</td>
  <td data-label="Status">Active</td>
</tr>

React Container Query Hook

typescript
// hooks/useContainerSize.ts
import { useRef, useState, useEffect } from 'react';

interface ContainerSize {
  width: number;
  height: number;
  inlineSize: number;
  blockSize: number;
}

export function useContainerSize<T extends HTMLElement>(): [
  React.RefObject<T>,
  ContainerSize
] {
  const ref = useRef<T>(null);
  const [size, setSize] = useState<ContainerSize>({
    width: 0,
    height: 0,
    inlineSize: 0,
    blockSize: 0,
  });

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const observer = new ResizeObserver(entries => {
      for (const entry of entries) {
        const { width, height } = entry.contentRect;
        const [borderBoxSize] = entry.borderBoxSize;

        setSize({
          width,
          height,
          inlineSize: borderBoxSize?.inlineSize ?? width,
          blockSize: borderBoxSize?.blockSize ?? height,
        });
      }
    });

    observer.observe(element);
    return () => observer.disconnect();
  }, []);

  return [ref, size];
}

// Usage: JavaScript-driven container queries (fallback or complex logic)
function ResponsiveCard() {
  const [containerRef, { width }] = useContainerSize<HTMLDivElement>();

  const layout = width >= 500 ? 'horizontal' : 'vertical';

  return (
    <div ref={containerRef} className="card-container">
      <div className={`card card--${layout}`}>
        {/* ... */}
      </div>
    </div>
  );
}

CSS Style Queries

Beyond size queries, CSS also supports style queries — responding to the computed style values of a container (currently limited to custom properties):

css
.card-container {
  container-type: normal; /* Style queries don't need size containment */
  container-name: card;
}

/* Style query: responds to a custom property on the container */
@container card style(--variant: featured) {
  .card {
    border: 2px solid var(--color-brand);
    background: var(--color-surface-brand);
  }

  .card__badge {
    display: block; /* Show "Featured" badge */
  }
}
html
<!-- Toggle featured variant via custom property -->
<div class="card-container" style="--variant: featured">
  <article class="card">
    <!-- Automatically styled as featured -->
  </article>
</div>

Container Queries vs. Media Queries

FeatureMedia QueriesContainer Queries
Responds toViewport dimensionsContainer dimensions
Component portabilityLow — tied to viewportHigh — works anywhere
NestingNot aware of contextFull context awareness
Browser supportUniversal90%+ (Chrome 105+, Firefox 110+, Safari 16+)
PerformanceStatic breakpointsDynamic via ResizeObserver internally
Use forPage-level layoutComponent-level layout

Use both

Container queries replace most uses of media queries for component-level responsive behavior. Media queries are still appropriate for:

  • Page layout (sidebar visible/hidden at screen level)
  • Loading different asset sizes (<source media="...">)
  • OS preferences (prefers-color-scheme, prefers-reduced-motion)
  • Print styles (@media print)

Nesting Container Queries

Container queries respect the nearest container ancestor:

css
.outer {
  container-type: inline-size;
  container-name: outer;
}

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

/* Targets the inner container's size */
@container inner (min-width: 300px) {
  .deep-child { font-size: 1.2rem; }
}

/* Targets the outer container's size */
@container outer (min-width: 800px) {
  .inner-element { max-width: 50%; }
}

Performance

Container queries are implemented using ResizeObserver internally by browsers. Performance characteristics:

  • Each container query rule adds an observer on the container element
  • ResizeObserver callbacks are debounced per frame (60fps max)
  • No layout thrashing — queries are evaluated in a single pass
  • More efficient than JavaScript-based responsive patterns

Benchmark: 100 container query rules on 100 elements = ~2ms overhead per resize event. Comparable to 100 CSS transitions.

War Story

A design system team was shipping 3 variants of every card component — "compact", "standard", and "wide" — as separate React components. When the product added a new layout context (a three-column feature grid), they needed to build a fourth "feature" variant. Using container queries, they were able to reduce to a single card component. The three component variants were deleted, saving 800 lines of code. The single component worked correctly in all 4 layout contexts automatically.

"What I cannot create, I do not understand." — Richard Feynman