ResizeObserver and Container Queries: Element-Level Responsive Layout Solutions

前端工程(Updated Jun 5, 2026)

From Viewport Responsive to Element Responsive

Traditional responsiveness is based on viewport width (@media), but components should adapt to their container size:

Aspect @media Viewport Query Container Query ResizeObserver
Responds to Viewport width Container width Element size
Scope Global Within container JS callback
CSS-driven
JS-driven
Component portability Poor Good Good
Performance Browser-optimized Browser-optimized Watch callback frequency

ResizeObserver API Deep Dive

Basic Usage

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    console.log(`${entry.target.className}: ${width}×${height}`);

    const borderBox = entry.borderBoxSize?.[0];
    console.log(`With border: ${borderBox?.inlineSize}×${borderBox?.blockSize}`);
  }
});

observer.observe(document.querySelector('.card'));

Observation Options

observer.observe(element, {
  box: 'content-box'  // Default, content area
  // box: 'border-box'  // Including padding + border
  // box: 'device-pixel-content-box'  // Device pixel level
});

Practice: Adaptive Card Layout

class ResponsiveCard {
  private observer: ResizeObserver;

  constructor(private card: HTMLElement) {
    this.observer = new ResizeObserver((entries) => {
      const { width } = entries[0].contentRect;
      this.applyLayout(width);
    });
    this.observer.observe(card);
  }

  private applyLayout(width: number) {
    if (width < 200) {
      this.card.setAttribute('data-layout', 'compact');
    } else if (width < 400) {
      this.card.setAttribute('data-layout', 'normal');
    } else {
      this.card.setAttribute('data-layout', 'expanded');
    }
  }

  destroy() {
    this.observer.disconnect();
  }
}
.card[data-layout="compact"] {
  flex-direction: column;
  font-size: 12px;
}
.card[data-layout="normal"] {
  flex-direction: column;
  font-size: 14px;
}
.card[data-layout="expanded"] {
  flex-direction: row;
  font-size: 16px;
}

Practice: Responsive Chart

class ResponsiveChart {
  private observer: ResizeObserver;
  private chart: Chart | null = null;

  constructor(private container: HTMLElement) {
    this.observer = new ResizeObserver((entries) => {
      const { width, height } = entries[0].contentRect;
      this.resizeChart(width, height);
    });
    this.observer.observe(container);
  }

  private resizeChart(width: number, height: number) {
    if (this.chart) {
      this.chart.resize({ width, height });
    } else {
      this.chart = new Chart(this.container, {
        type: 'line',
        data: this.getChartData(width),
        options: {
          responsive: false,
          maintainAspectRatio: false,
        }
      });
    }
  }

  private getChartData(width: number) {
    const pointCount = width < 300 ? 5 : width < 600 ? 10 : 20;
    return { labels: generateLabels(pointCount), datasets: [...] };
  }

  destroy() {
    this.observer.disconnect();
    this.chart?.destroy();
  }
}

Practice: Text Truncation Detection

class TextOverflowDetector {
  private observer: ResizeObserver;

  constructor(
    private element: HTMLElement,
    private onOverflow: (isOverflow: boolean) => void
  ) {
    this.observer = new ResizeObserver(() => {
      const isOverflow = this.checkOverflow();
      this.onOverflow(isOverflow);
    });
    this.observer.observe(element);
  }

  private checkOverflow(): boolean {
    return this.element.scrollWidth > this.element.clientWidth;
  }

  destroy() {
    this.observer.disconnect();
  }
}

const detector = new TextOverflowDetector(
  titleElement,
  (overflow) => {
    tooltipElement.style.display = overflow ? 'inline' : 'none';
  }
);

CSS Container Queries

Basic Syntax

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

@container card (min-width: 400px) {
  .card {
    flex-direction: row;
    gap: 16px;
  }
  .card-title {
    font-size: 20px;
  }
}

@container card (min-width: 200px) and (max-width: 399px) {
  .card {
    flex-direction: column;
  }
  .card-title {
    font-size: 16px;
  }
}

@container card (max-width: 199px) {
  .card-title {
    font-size: 14px;
  }
  .card-meta {
    display: none;
  }
}

Container Query Units

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

.card-title {
  font-size: clamp(14px, 5cqi, 24px);
  padding: 2cqi;
}
Unit Meaning Full name
cqw 1% of container width container-query-width
cqh 1% of container height container-query-height
cqi 1% of container inline size container-query-inline
cqb 1% of container block size container-query-block
cqmin min(cqi, cqb) Smaller dimension
cqmax max(cqi, cqb) Larger dimension

Container Queries vs Media Queries

Same Component, Different Contexts

<div class="sidebar">
  <product-card />  <!-- Sidebar: narrow container → compact layout -->
</div>

<div class="main-content">
  <product-card />  <!-- Main: wide container → expanded layout -->
</div>
.product-card-container {
  container-type: inline-size;
}

@container (min-width: 350px) {
  .product-card { flex-direction: row; }
}

@container (max-width: 349px) {
  .product-card { flex-direction: column; }
}

Media queries can't do this: same component auto-adapting in different positions.

Comparison Summary

Scenario @media @container
Global layout (nav/footer) ✅ Appropriate ❌ Not needed
Component adaptation ❌ Manual breakpoints ✅ Auto-adapts
Component reusability Poor (viewport-dependent) Good (container-dependent)
Nested components ❌ Can't sense parent ✅ Adapts per level
Performance Viewport change triggers Container change triggers

ResizeObserver + Container Query Combined

class SmartResponsive {
  private observer: ResizeObserver;

  constructor(private container: HTMLElement) {
    this.observer = new ResizeObserver((entries) => {
      const { width } = entries[0].contentRect;

      if (width < 300) {
        this.showSummary();
      } else {
        this.showFullData();
      }
    });

    this.observer.observe(container);
  }

  private showSummary() {
    this.container.setAttribute('data-density', 'sparse');
  }

  private showFullData() {
    this.container.setAttribute('data-density', 'dense');
  }

  destroy() {
    this.observer.disconnect();
  }
}

Performance Optimization

Debounced ResizeObserver

function debouncedResizeObserver(
  callback: (entries: ResizeObserverEntry[]) => void,
  delay = 100
): ResizeObserver {
  let timer: number;
  let latestEntries: ResizeObserverEntry[];

  return new ResizeObserver((entries) => {
    latestEntries = entries;
    clearTimeout(timer);
    timer = window.setTimeout(() => callback(latestEntries), delay);
  });
}

Batch Processing

const observer = new ResizeObserver((entries) => {
  requestAnimationFrame(() => {
    for (const entry of entries) {
      updateLayout(entry.target as HTMLElement, entry.contentRect);
    }
  });
});

Browser Compatibility

API Chrome Firefox Safari Edge
ResizeObserver 64+ 69+ 13.1+ 79+
Container Queries 105+ 110+ 16+ 105+
Container Units (cqw/cqi) 105+ 110+ 16+ 105+
container-type 105+ 110+ 16+ 105+
borderBoxSize 84+ 69+ 13.1+ 84+

Best Practices

  1. Prefer Container Query: Use CSS container queries for pure layout adaptation—no JS needed
  2. ResizeObserver for logic: Use JS when data/behavior changes are needed
  3. Disconnect promptly: Unobserve on component unmount to prevent leaks
  4. Avoid loops: Modifying size in callbacks can trigger new callbacks—debounce
  5. container-type choice: Use inline-size for width-only adaptation—better perf than size

Summary

Element-level responsiveness is a core requirement of modern component design. CSS Container Queries enable pure-CSS container adaptation, ResizeObserver provides precise JS size awareness, and together they form a complete element-level responsive solution. Say goodbye to "hardcoded viewport breakpoints"—components truly adapt wherever they're placed.

Use Flexbox Generator for in-container layouts, Gradient Generator for responsive backgrounds, and Box Shadow Generator to debug card styles within containers.

Try these browser-local tools — no sign-up required →

#ResizeObserver#响应式#容器查询#元素尺寸#布局