Container Queries

Difficulty: Advanced

Container queries are a revolutionary CSS feature that allows elements to respond to the size of their container rather than the viewport. While media queries ask "how wide is the browser window?", container queries ask "how wide is my parent container?" This fundamentally changes how we build responsive components, because a component can now adapt its layout based on the space available to it, regardless of where it is placed on the page.

To use container queries, you first define a containment context on the parent element using the container-type property. Setting container-type: inline-size enables queries based on the container's inline (horizontal) dimension. You can also use container-type: size for both dimensions, though inline-size is far more common. The shorthand container property lets you set both the type and an optional name: container: sidebar / inline-size.

Once a container is established, you use the @container rule (similar to @media) to write conditional styles: @container (min-width: 400px) { /* styles */ }. These styles apply when the container - not the viewport - reaches 400px width. This means a card component can show a horizontal layout in a wide sidebar and a vertical layout in a narrow sidebar, all without any knowledge of the viewport width.

Container names let you target specific containers when you have nested containment contexts. Without a name, @container matches the nearest ancestor container. With a name - @container sidebar (min-width: 300px) - you explicitly target the container named "sidebar". This prevents ambiguity in complex layouts where multiple containers are nested.

Container queries also introduce container query units: cqw (1% of container width), cqh (1% of container height), cqi (1% of container inline size), cqb (1% of container block size), cqmin (smaller of cqi/cqb), and cqmax (larger of cqi/cqb). These work like viewport units but are relative to the container, enabling truly fluid component-level sizing without any reference to the viewport at all.

Code examples

Basic Container Query Setup

<style>
  /* Establish containment context */
  .card-wrapper {
    container-type: inline-size;
  }

  /* Base styles (narrow container) */
  .card {
    display: flex;
    flex-direction: column;
    background: white;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
    overflow: hidden;
  }
  .card img {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }
  .card-body {
    padding: 16px;
  }

  /* When container is wider than 400px */
  @container (min-width: 400px) {
    .card {
      flex-direction: row;
    }
    .card img {
      width: 200px;
      height: auto;
    }
  }

  /* When container is wider than 600px */
  @container (min-width: 600px) {
    .card-body {
      padding: 24px;
    }
    .card h3 {
      font-size: 1.5rem;
    }
  }
</style>

<div class="card-wrapper" style="width: 350px;">
  <div class="card">
    <img src="photo.jpg" alt="Product">
    <div class="card-body">
      <h3>Product Name</h3>
      <p>Product description text.</p>
    </div>
  </div>
</div>

The card-wrapper has container-type: inline-size, making it a containment context. The card responds to its container's width, not the viewport. At 400px container width, it switches to a horizontal layout.

Named Containers

<style>
  /* Named containers for explicit targeting */
  .main-area {
    container: main-content / inline-size;
    flex: 1;
    padding: 24px;
  }
  .sidebar {
    container: sidebar / inline-size;
    width: 300px;
    padding: 16px;
    background: #f8fafc;
  }

  /* Target the main content container specifically */
  @container main-content (min-width: 600px) {
    .article-grid {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 16px;
    }
  }

  @container main-content (min-width: 900px) {
    .article-grid {
      grid-template-columns: repeat(3, 1fr);
    }
  }

  /* Target the sidebar container specifically */
  @container sidebar (min-width: 250px) {
    .sidebar-card {
      display: flex;
      gap: 12px;
      align-items: center;
    }
  }
</style>

<div style="display: flex;">
  <div class="main-area">
    <div class="article-grid">
      <div>Article 1</div>
      <div>Article 2</div>
      <div>Article 3</div>
    </div>
  </div>
  <div class="sidebar">
    <div class="sidebar-card">
      <img src="thumb.jpg" alt="" width="60">
      <span>Related Post</span>
    </div>
  </div>
</div>

Named containers (container: name / inline-size) prevent ambiguity. @container main-content targets only the main area, not the sidebar. This is essential when components appear in different containers with different widths.

Container Query Length Units

<style>
  .widget-container {
    container-type: inline-size;
    padding: 20px;
    border: 1px solid #e2e8f0;
  }

  .widget-title {
    /* Font size relative to container width */
    font-size: clamp(1rem, 5cqi, 2rem);
    margin-bottom: 0.5em;
  }

  .widget-grid {
    display: grid;
    /* Columns sized relative to container */
    grid-template-columns: repeat(auto-fit, minmax(20cqi, 1fr));
    gap: 2cqi;
  }

  .widget-item {
    padding: 3cqi;
    background: #f1f5f9;
    border-radius: 8px;
  }

  /* cqi = 1% of container inline size
     cqb = 1% of container block size
     cqw = 1% of container width
     cqh = 1% of container height
     cqmin = smaller of cqi/cqb
     cqmax = larger of cqi/cqb */
</style>

<div class="widget-container" style="width: 500px;">
  <h2 class="widget-title">Dashboard Widget</h2>
  <div class="widget-grid">
    <div class="widget-item">Item A</div>
    <div class="widget-item">Item B</div>
    <div class="widget-item">Item C</div>
  </div>
</div>

Container query units (cqi, cqb, cqw, cqh, cqmin, cqmax) work like viewport units but are relative to the container. 5cqi = 5% of the container's inline size. They enable fully fluid component-level sizing.

Reusable Component with Container Queries

<style>
  /* Containment on any parent that uses this component */
  .product-slot {
    container-type: inline-size;
  }

  /* The component adapts to its container */
  .product-card {
    display: grid;
    gap: 12px;
    padding: 16px;
    background: white;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
  }

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

  /* Narrow: image on top, text below (default) */
  .product-card .info { text-align: center; }
  .product-card .price { font-size: 1.25rem; }

  /* Medium: horizontal layout */
  @container (min-width: 350px) {
    .product-card {
      grid-template-columns: 150px 1fr;
      align-items: center;
    }
    .product-card img {
      aspect-ratio: 1;
    }
    .product-card .info { text-align: left; }
  }

  /* Wide: prominent layout */
  @container (min-width: 500px) {
    .product-card {
      grid-template-columns: 250px 1fr;
      padding: 20px;
    }
    .product-card .price {
      font-size: 1.5rem;
      color: #16a34a;
    }
  }
</style>

<!-- Same component, different containers -->
<div style="display: flex; gap: 20px;">
  <div class="product-slot" style="width: 280px;">
    <div class="product-card">
      <img src="product.jpg" alt="Product">
      <div class="info">
        <h3>Widget Pro</h3>
        <p class="price">$49.99</p>
      </div>
    </div>
  </div>
  <div class="product-slot" style="flex: 1;">
    <div class="product-card">
      <img src="product.jpg" alt="Product">
      <div class="info">
        <h3>Widget Pro</h3>
        <p class="price">$49.99</p>
      </div>
    </div>
  </div>
</div>

The same product-card component renders differently based on its container width. In a narrow sidebar (280px), it stacks vertically. In a wide main area, it switches to a horizontal layout with larger pricing. The component is completely self-contained.

Key points

Concepts covered

@container, container-type, container-name, Container Units, Component-Level Responsiveness