CSS Container Queries for Component Libraries

· 8 min read · CSS

Use CSS container queries to build components that respond to their container size rather than viewport width.

CSS Container Queries for Component Libraries

Media queries respond to viewport size, but components are often used in contexts where viewport size does not reflect available space—sidebars, cards, dashboard widgets. Container queries allow components to respond to their container's size, enabling truly responsive component design.

Problem

A card component designed for a wide layout breaks when placed in a narrow sidebar:

.card {
  display: grid;
  grid-template-columns: 200px 1fr;
}

@media (max-width: 768px) {
  .card {
    grid-template-columns: 1fr;
  }
}

On a 1200px viewport with a 300px sidebar, the card is in a narrow container but media queries see a wide viewport. The card stays in horizontal layout despite not fitting.

Why It Happens

Media queries measure the viewport, not the component's container. A component cannot know whether it is in a full-width layout, a sidebar, a modal, or a grid cell. The same viewport width can mean different available space depending on context.

Solution

Use CSS container queries to make components respond to their parent container's size.

Implementation

Establishing Containment

First, establish containment on the parent:

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

Or shorthand:

.container {
  container: card-container / inline-size;
}

Querying the Container

.card {
  display: grid;
  grid-template-columns: 1fr;
}

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

When the container is at least 500px wide, the card uses a two-column layout. When narrower, it stacks.

Named Containers

For complex layouts with nested containers:

.sidebar {
  container: sidebar / inline-size;
}

.card-grid {
  container: card-grid / inline-size;
}

/* Query specific container */
@container sidebar (max-width: 300px) {
  .sidebar-nav {
    /* Compact navigation */
  }
}

@container card-grid (min-width: 600px) {
  .card {
    /* Expanded layout */
  }
}

Example

Responsive card component:

/* Card wrapper establishes containment */
.card-wrapper {
  container-type: inline-size;
}

/* Default: compact layout */
.card {
  display: flex;
  flex-direction: column;
  padding: 1rem;
}

.card-image {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.card-content {
  padding: 1rem 0;
}

/* Wide layout when container >= 400px */
@container (min-width: 400px) {
  .card {
    flex-direction: row;
    gap: 1.5rem;
  }
  
  .card-image {
    width: 40%;
    aspect-ratio: 1;
  }
}

/* Full layout when container >= 600px */
@container (min-width: 600px) {
  .card {
    padding: 1.5rem;
  }
  
  .card-image {
    width: 200px;
  }
  
  .card-title {
    font-size: 1.5rem;
  }
}

Usage:

<!-- Full width: gets wide layout -->
<div class="card-wrapper">
  <div class="card">...</div>
</div>

<!-- Inside sidebar: gets compact layout -->
<aside class="sidebar">
  <div class="card-wrapper">
    <div class="card">...</div>
  </div>
</aside>

TIP: Always wrap components in a container element. You cannot query the size of the element with container-type itself, only its descendants can query it.

Common Mistakes

Querying the element itself

/* Does NOT work */
.card {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card {
    /* Cannot query own container */
  }
}

The query matches the nearest ancestor with container-type, not the element itself.

Fix:

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

@container (min-width: 400px) {
  .card {
    /* Now queries .card-wrapper */
  }
}

Breaking layout containment

container-type: inline-size establishes containment. Some layouts break:

.container {
  container-type: inline-size;
  /* Elements inside cannot affect container's inline size */
}

.child {
  width: max-content; /* May not work as expected */
}

WARNING: Size containment means the container's size cannot depend on its contents. If you need intrinsic sizing, this may cause issues.

Not providing fallbacks

Container queries have good browser support but may need fallbacks:

/* Fallback for older browsers */
.card {
  display: flex;
  flex-direction: column;
}

/* Container query for modern browsers */
@container (min-width: 400px) {
  .card {
    flex-direction: row;
  }
}

Older browsers ignore the @container rule and use the base styles.

Forgetting container-type on wrapper

/* Missing container-type */
.wrapper {
  /* No containment */
}

@container (min-width: 500px) {
  .child {
    /* Queries nearest ancestor with containment - may be unexpected */
  }
}

Always explicitly set container-type on the intended container.

Conclusion

Container queries enable truly responsive components that adapt to their context rather than the viewport. Establish containment on wrapper elements, write queries for breakpoints that make sense for the component's layout, and test components in various container sizes. This approach makes component libraries more robust and context-aware.