10LOC
Web Platformintermediate

ResizeObserver for a responsive component with no resize listener

Published September 15, 2026

const setLayout = (el: HTMLElement, width: number) => {
  el.classList.toggle("is-wide", width >= 560);
  el.classList.toggle("is-compact", width < 320);
};

export const observeResponsiveWidth = (el: HTMLElement) => {
  const observer = new ResizeObserver(([entry]) => setLayout(el, entry.contentRect.width));
  observer.observe(el);
  return () => observer.disconnect();
};

What

observeResponsiveWidth watches one element's own box and toggles layout classes based on its width — not the viewport's — whenever that element is resized, for any reason.

Why it matters

Media queries and window.addEventListener("resize", ...) both answer "how wide is the viewport?" That's the wrong question for a component that can be dropped into a narrow sidebar or a wide main column on the same page at the same viewport width — a card that's "wide" in a 3-column grid and "compact" in a 1-column one needs to know its own size, not the window's.

A resize listener also fires for viewport changes only, fires synchronously on the main thread, and gives you no way to watch a specific element without manually diffing its getBoundingClientRect() yourself on every event. ResizeObserver watches a specific box and only calls back when that box's size actually changes, whether that's from a window resize, a sidebar collapsing, a grid reflow, or a parent's font-size changing.

How it works

  • observer.observe(el) starts watching. The callback receives an array of ResizeObserverEntry — one per observed target — with [entry] destructuring the single one here.
  • entry.contentRect.width is the element's content-box width (padding and border excluded), already computed by the browser — no getBoundingClientRect() call needed.
  • setLayout uses classList.toggle(name, force)'s two-argument form: pass the condition directly instead of branching on it, so the class list always matches the current width with no stale state.
  • The returned cleanup function calls observer.disconnect(), which stops watching every observed target on this observer. Call it when the component unmounts — an active ResizeObserver on a removed element is a real, easy-to-miss memory leak.

Gotchas

  • The callback fires once immediately after the first observe() call, with the element's current size — treat that as the initial layout pass, not a spurious extra event.
  • entry.contentRect doesn't include padding or border. If your breakpoints should account for those, use entry.borderBoxSize[0] instead (an array, because a target can have multiple fragments in multi-column layouts).
  • Resizing an element from inside its own resize callback (e.g. changing a size that feeds back into itself) can trigger ResizeObserver loop warnings. Only touch classes/attributes that don't themselves affect the observed box's dimensions.

Related

  • IntersectionObserver — same async, callback-driven shape, but for viewport visibility instead of size.
  • CSS container queries — solve the same "respond to a container's size" problem in pure CSS when you don't need a JS-computed breakpoint or a side-effect beyond styling.