Container Query Responsive Design

Frontend Engineering advanced Building with AI ~22 min read

Overview

For over a decade, responsive web design meant one thing: media queries that respond to the viewport width. A card component would switch from horizontal to vertical layout at 768px because that was the breakpoint where the page happened to have a sidebar. But the card itself doesn’t know about the sidebar — it only knows about the viewport. If you move that same card into a narrower column, a modal, or a dashboard widget, the media query fires at the wrong breakpoint because the available space changed but the viewport didn’t. Container queries fix this fundamental mismatch by letting components respond to the size of their parent container instead of the viewport.

CSS Container Queries, specified in CSS Containment Level 3, introduce a new responsive primitive: @container rules that evaluate against a containment context rather than the viewport. A parent element declares itself as a container with container-type: inline-size, and child elements can query the container’s width with @container (min-width: 400px). The component adapts to its available space regardless of where it’s placed in the page — sidebar, main content, modal, or embedded widget.

Tailwind CSS v4 adds first-class support for container queries through the @container variant prefix. Instead of md:flex-row (viewport-based), you write @md:flex-row (container-based), and the component responds to its container’s width. The internal component library uses this extensively for sidebar-aware layouts where the main content area’s available width depends on whether a collapsible sidebar is open — a scenario where viewport media queries produce incorrect results.

Core Concepts

Containment Context

A container query requires an ancestor element to establish a containment context via container-type: inline-size (or size for both axes). This tells the browser to track the element’s dimensions and make them queryable by descendants. The contained element establishes a new layout boundary — its size is determined by its own constraints and content, not by the queries of its children. This prevents circular dependencies where a child’s layout affects the container’s size, which would change the query result, which would change the child’s layout.

Container vs. Viewport Queries

Media queries (@media (min-width: 768px)) check the viewport. Container queries (@container (min-width: 400px)) check the nearest ancestor with container-type set. The difference matters whenever a component’s available space differs from the viewport width — which is most of the time in real applications. Sidebars, multi-column layouts, modals, popovers, embedded widgets, and CMS content blocks all create contexts where viewport width is an unreliable proxy for available space.

Named Containers

When multiple containment contexts are nested, @container queries match the nearest one by default. Named containers (container-name: sidebar) let you query a specific ancestor: @container sidebar (min-width: 300px). This is essential in complex layouts where a component inside a sidebar card needs to query the sidebar’s width, not the card’s width. Tailwind v4 supports named containers with the @container/name syntax.

Container Query Units

CSS introduces new units relative to the container’s dimensions: cqw (1% of container width), cqh (1% of container height), cqi (1% of inline size), and cqb (1% of block size). These units let you size elements proportionally to their container — a heading set to font-size: 3cqi scales with its container’s width, creating fluid typography without viewport-based clamp() calculations.

Intrinsic Component Design

Container queries enable truly intrinsic component design — components that define their own responsive behavior based on the space they’re given, independent of the page layout. A ProductCard knows “below 300px I stack vertically, above 300px I go horizontal” regardless of whether it’s in a 3-column grid, a sidebar, or a modal. This makes components genuinely portable across layouts and applications.

How It Works

Basic Container Query Setup

The parent declares containment, and children query the container’s dimensions. This is the minimal CSS needed to make a component container-aware.

/* The parent establishes a containment context */
.content-area {
  container-type: inline-size;
  container-name: main;
}

/* The child queries the container, not the viewport */
.result-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* When the container is wide enough, switch to horizontal */
@container main (min-width: 500px) {
  .result-card {
    flex-direction: row;
    align-items: center;
  }

  .result-card__image {
    width: 200px;
    flex-shrink: 0;
  }
}

/* Full-width layout when container is very wide */
@container main (min-width: 800px) {
  .result-card {
    gap: 24px;
  }

  .result-card__meta {
    display: flex;
    gap: 16px;
  }
}

Tailwind v4 Container Query Syntax

Tailwind v4 provides the @container utility for declaring containment and the @ prefix for container-responsive variants, mirroring the familiar sm:/md:/lg: viewport breakpoints.

// Sidebar-aware layout in the component library
function DashboardLayout({ children }: { children: React.ReactNode }) {
  const [sidebarOpen, setSidebarOpen] = React.useState(true);

  return (
    <div className="flex h-screen">
      {/* Collapsible sidebar */}
      <aside className={cn(
        "transition-all duration-300 border-r border-border",
        sidebarOpen ? "w-64" : "w-16"
      )}>
        {/* sidebar content */}
      </aside>

      {/* Main content area is a container */}
      <main className="flex-1 @container overflow-auto p-6">
        {children}
      </main>
    </div>
  );
}

// Components inside respond to main's width, not viewport
function SearchResults({ results }: { results: Result[] }) {
  return (
    <div className="grid gap-4 @sm:grid-cols-1 @md:grid-cols-2 @xl:grid-cols-3">
      {results.map((result) => (
        <ResultCard
          key={result.id}
          className="@container"  {/* Each card is also a container */}
        >
          <div className="flex flex-col @xs:flex-row gap-3">
            <img
              src={result.thumbnail}
              className="w-full @xs:w-24 @xs:h-24 rounded-md object-cover"
            />
            <div className="flex-1">
              <h3 className="font-semibold text-sm @sm:text-base">
                {result.title}
              </h3>
              <p className="text-muted-foreground text-xs @sm:text-sm line-clamp-2">
                {result.snippet}
              </p>
            </div>
          </div>
        </ResultCard>
      ))}
    </div>
  );
}

Named Containers for Complex Layouts

Named containers let deeply nested components query a specific ancestor, which is critical when containers are nested multiple levels deep.

/* CSS for named containers */
.dashboard-panel {
  container-type: inline-size;
  container-name: panel;
}

.dashboard-panel .widget {
  container-type: inline-size;
  container-name: widget;
}

/* Query the widget container (nearest) */
@container widget (min-width: 250px) {
  .chart-legend {
    display: flex;
    flex-wrap: wrap;
  }
}

/* Query the panel container (named, skips widget) */
@container panel (min-width: 600px) {
  .widget-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Tailwind v4 named container syntax */
/* <div className="@container/panel">  → container-name: panel  */
/* <div className="@md/panel:grid-cols-2"> → @container panel (min-width: ...) */

Practical Applications

The internal component library’s primary use case for container queries is the sidebar-aware dashboard layout. The admin dashboard has a collapsible sidebar (64px collapsed, 256px expanded). When the sidebar toggles, the main content area’s width changes by ~192px — enough to shift a 3-column grid to 2 columns on a 1280px monitor. With viewport media queries, this transition wouldn’t happen because the viewport width hasn’t changed. Container queries on the main content area detect the actual available width and reflow the grid correctly.

The search results page uses nested container queries: the results grid is a container that determines column count, and each result card is also a container that determines whether to display in horizontal or vertical layout. A narrow viewport with no sidebar might show 1 column of horizontal cards, while a wide viewport with sidebar might show 2 columns of vertical cards — the same component in both cases, adapting to its actual space.

Embeddable widgets distributed to merchant sites rely entirely on container queries because the widget has no control over the viewport. A search bar widget might be placed in a 300px sidebar or a 900px hero section. Container queries let the widget adapt its layout, result count, and even the level of detail shown in autocomplete suggestions based on the container it’s placed in — making the widget truly portable across merchant sites without per-site CSS customization.

Implementation Patterns

Progressive Enhancement with Fallback

Container queries have broad browser support (Chrome 105+, Safari 16+, Firefox 110+), but older browsers need a graceful fallback. Use @supports to detect support and provide viewport-based defaults.

/* Mobile-first base (works everywhere) */
.product-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

/* Viewport fallback for browsers without container queries */
@media (min-width: 768px) {
  .product-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1200px) {
  .product-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* Container query override for supporting browsers */
@supports (container-type: inline-size) {
  .product-grid-container {
    container-type: inline-size;
  }

  /* Reset the media query rules */
  .product-grid {
    grid-template-columns: 1fr;
  }

  @container (min-width: 500px) {
    .product-grid {
      grid-template-columns: repeat(2, 1fr);
    }
  }

  @container (min-width: 800px) {
    .product-grid {
      grid-template-columns: repeat(3, 1fr);
    }
  }
}

Container Query Units for Fluid Typography

Container query units enable typography that scales with its container, creating fluid headings without viewport-relative calculations.

/* Container-relative fluid typography */
.hero-section {
  container-type: inline-size;
}

.hero-section h1 {
  /* Fluid between 24px and 48px based on container width */
  font-size: clamp(1.5rem, 5cqi, 3rem);
  line-height: 1.1;
}

.hero-section .subtitle {
  font-size: clamp(0.875rem, 2cqi, 1.25rem);
  max-width: 60cqi; /* 60% of container width */
}

/* Stats counter that scales with its container */
.stat-card {
  container-type: inline-size;
}

.stat-card .number {
  font-size: clamp(2rem, 8cqi, 4rem);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

Common Pitfalls

Further Reading