CSS Container Queries: The Future of Responsive Design is Here

CSS Container Queries have finally arrived in all major browsers, and they're about to revolutionize how we think about responsive design. After extensively using them in production, I can confidently say they solve problems we've been working around for years.

Why Container Queries Matter

Traditional media queries respond to viewport size, but components often need to respond to their container size instead:

/* Old way: Media queries based on viewport */
@media (min-width: 768px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
}

/* New way: Container queries based on container size */
@container (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
}

This fundamental shift enables truly modular, reusable components that adapt to their context rather than the entire viewport.

Setting Up Container Queries

Basic Container Setup

.card-container {
  container-type: inline-size; /* Queries based on width */
}

.sidebar-container {
  container-type: block-size; /* Queries based on height */
}

.flexible-container {
  container-type: size; /* Queries based on both dimensions */
}

Named Containers

.product-grid {
  container-name: product-layout;
  container-type: inline-size;
}

/* Shorthand syntax */
.product-grid {
  container: product-layout / inline-size;
}

/* Query the named container */
@container product-layout (min-width: 600px) {
  .product-card {
    grid-template-columns: repeat(3, 1fr);
  }
}

Practical Design Patterns

Responsive Card Components

.card-container {
  container-type: inline-size;
  padding: 1rem;
}

.card {
  background: white;
  border-radius: 8px;
  padding: 1rem;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

/* Small container: stacked layout */
@container (max-width: 300px) {
  .card {
    text-align: center;
  }
  
  .card__image {
    width: 100%;
    margin-bottom: 1rem;
  }
  
  .card__content {
    width: 100%;
  }
}

/* Medium container: side-by-side layout */
@container (min-width: 301px) and (max-width: 500px) {
  .card {
    display: flex;
    align-items: center;
    gap: 1rem;
  }
  
  .card__image {
    width: 120px;
    flex-shrink: 0;
  }
  
  .card__content {
    flex: 1;
  }
}

/* Large container: enhanced layout */
@container (min-width: 501px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr auto;
    gap: 1.5rem;
    align-items: center;
  }
  
  .card__actions {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }
}

Navigation Patterns

.navigation {
  container-type: inline-size;
}

/* Compact navigation for small containers */
@container (max-width: 600px) {
  .nav-list {
    display: none;
  }
  
  .nav-toggle {
    display: block;
  }
  
  .nav-list.is-open {
    display: flex;
    flex-direction: column;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: white;
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  }
}

/* Full navigation for larger containers */
@container (min-width: 601px) {
  .nav-list {
    display: flex;
    gap: 2rem;
  }
  
  .nav-toggle {
    display: none;
  }
}

Data Visualization Adaptations

.chart-container {
  container-type: size;
}

/* Simplified chart for small containers */
@container (max-width: 400px) {
  .chart {
    --chart-type: 'bar';
  }
  
  .chart-legend {
    display: none;
  }
  
  .chart-labels {
    font-size: 0.75rem;
  }
}

/* Enhanced chart for medium containers */
@container (min-width: 401px) and (max-width: 800px) {
  .chart {
    --chart-type: 'line';
  }
  
  .chart-legend {
    display: flex;
    justify-content: center;
    margin-top: 1rem;
  }
}

/* Full-featured chart for large containers */
@container (min-width: 801px) {
  .chart {
    --chart-type: 'advanced';
  }
  
  .chart-legend {
    position: absolute;
    right: 0;
    top: 0;
  }
  
  .chart-controls {
    display: block;
  }
}

Advanced Container Query Techniques

Container Query Units

.dynamic-component {
  container-type: size;
}

@container (min-width: 300px) {
  .responsive-text {
    /* Container query units */
    font-size: 4cqw; /* 4% of container width */
    padding: 2cqh 3cqw; /* 2% of container height, 3% of container width */
    
    /* Available 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 or cqb
       cqmax - larger of cqi or cqb */
  }
}

Nested Container Queries

.page-layout {
  container-name: page;
  container-type: inline-size;
}

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

/* Page-level adaptations */
@container page (min-width: 1200px) {
  .main-content {
    display: grid;
    grid-template-columns: 300px 1fr;
  }
}

/* Sidebar-level adaptations */
@container sidebar (min-width: 250px) {
  .sidebar-widget {
    padding: 1.5rem;
  }
  
  .sidebar-widget__title {
    font-size: 1.25rem;
  }
}

/* Combined conditions */
@container page (min-width: 1200px) {
  @container sidebar (min-width: 250px) {
    .sidebar-widget {
      border-right: 1px solid #eee;
    }
  }
}

Style Queries (Experimental)

/* Query based on custom properties */
.theme-container {
  container-name: theme;
}

@container style(--theme: dark) {
  .card {
    background: #1a1a1a;
    color: white;
  }
}

@container style(--layout: grid) {
  .items {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  }
}

Integration with CSS Grid and Flexbox

Grid Layout Adaptations

.product-grid {
  container-type: inline-size;
  display: grid;
  gap: 1rem;
}

@container (max-width: 400px) {
  .product-grid {
    grid-template-columns: 1fr;
  }
}

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

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

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

Flexbox Adaptations

.toolbar {
  container-type: inline-size;
  display: flex;
  gap: 0.5rem;
}

@container (max-width: 500px) {
  .toolbar {
    flex-direction: column;
  }
  
  .toolbar__actions {
    order: -1;
  }
}

@container (min-width: 501px) {
  .toolbar {
    justify-content: space-between;
    align-items: center;
  }
}

JavaScript Integration

Dynamic Container Setup

class ResponsiveComponent {
  constructor(element) {
    this.element = element
    this.setupContainerQueries()
    this.observeChanges()
  }
  
  setupContainerQueries() {
    // Dynamically set container type based on content
    const hasVariableWidth = this.element.dataset.responsive === 'width'
    const hasVariableHeight = this.element.dataset.responsive === 'height'
    
    if (hasVariableWidth && hasVariableHeight) {
      this.element.style.containerType = 'size'
    } else if (hasVariableWidth) {
      this.element.style.containerType = 'inline-size'
    } else if (hasVariableHeight) {
      this.element.style.containerType = 'block-size'
    }
  }
  
  observeChanges() {
    // Use ResizeObserver to track container size changes
    const resizeObserver = new ResizeObserver(entries => {
      for (const entry of entries) {
        const { inlineSize, blockSize } = entry.contentBoxSize[0]
        
        // Trigger custom events based on size thresholds
        if (inlineSize < 300) {
          this.element.dispatchEvent(new CustomEvent('container:small'))
        } else if (inlineSize > 800) {
          this.element.dispatchEvent(new CustomEvent('container:large'))
        }
      }
    })
    
    resizeObserver.observe(this.element)
  }
}

// Usage
document.querySelectorAll('[data-responsive]').forEach(element => {
  new ResponsiveComponent(element)
})

Container Query Polyfill

// Feature detection and polyfill loading
if (!CSS.supports('container-type: inline-size')) {
  import('container-query-polyfill').then(() => {
    console.log('Container queries polyfilled')
  })
}

// Progressive enhancement
function enhanceWithContainerQueries() {
  if (CSS.supports('container-type: inline-size')) {
    document.documentElement.classList.add('supports-container-queries')
  } else {
    // Fallback to ResizeObserver-based solution
    implementContainerQueryFallback()
  }
}

function implementContainerQueryFallback() {
  const containers = document.querySelectorAll('[data-container-query]')
  
  containers.forEach(container => {
    const resizeObserver = new ResizeObserver(entries => {
      const { inlineSize } = entries[0].contentBoxSize[0]
      
      // Apply classes based on size breakpoints
      container.classList.toggle('container-sm', inlineSize < 400)
      container.classList.toggle('container-md', inlineSize >= 400 && inlineSize < 800)
      container.classList.toggle('container-lg', inlineSize >= 800)
    })
    
    resizeObserver.observe(container)
  })
}

Migration Strategies

From Media Queries to Container Queries

/* Before: Media query approach */
@media (min-width: 768px) {
  .sidebar .widget {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
}

/* After: Container query approach */
.sidebar {
  container-type: inline-size;
}

@container (min-width: 300px) {
  .widget {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
}

Hybrid Approach

/* Use media queries for layout-level changes */
@media (min-width: 1024px) {
  .main-layout {
    display: grid;
    grid-template-columns: 300px 1fr;
  }
}

/* Use container queries for component-level changes */
.card-container {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card {
    display: flex;
    align-items: center;
  }
}

Performance Considerations

Optimizing Container Query Performance

/* Avoid unnecessary container types */
.efficient-container {
  /* Only use the container type you need */
  container-type: inline-size; /* Not 'size' if you only need width */
}

/* Group related queries */
@container (min-width: 400px) {
  .component-a { /* styles */ }
  .component-b { /* styles */ }
  .component-c { /* styles */ }
}

/* Rather than separate queries for each */

Browser Support and Fallbacks

/* Progressive enhancement approach */
.component {
  /* Base styles that work everywhere */
  display: block;
}

/* Enhanced styles with container queries */
@supports (container-type: inline-size) {
  .component-container {
    container-type: inline-size;
  }
  
  @container (min-width: 400px) {
    .component {
      display: grid;
      grid-template-columns: 1fr 2fr;
    }
  }
}

Real-World Examples

E-commerce Product Grid

.product-grid {
  container: products / inline-size;
  display: grid;
  gap: 1rem;
  padding: 1rem;
}

@container products (max-width: 400px) {
  .product-grid {
    grid-template-columns: 1fr;
  }
  
  .product-card {
    text-align: center;
  }
  
  .product-card__image {
    width: 100%;
    max-width: 200px;
  }
}

@container products (min-width: 401px) and (max-width: 700px) {
  .product-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@container products (min-width: 701px) and (max-width: 1000px) {
  .product-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

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

Conclusion

Container Queries represent a paradigm shift in responsive design. They enable truly modular components that adapt to their context, making our CSS more maintainable and our components more reusable.

Key takeaways:

  • Start with container-type: inline-size for most use cases
  • Use named containers for complex layouts
  • Combine with existing responsive techniques
  • Progressive enhancement ensures broad browser support
  • Performance impact is minimal with proper usage

The future of responsive design is contextual, and Container Queries are leading the way. Start experimenting with them today—your components (and your users) will thank you.