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 ofResizeObserverEntry— one per observed target — with[entry]destructuring the single one here.entry.contentRect.widthis the element's content-box width (padding and border excluded), already computed by the browser — nogetBoundingClientRect()call needed.setLayoutusesclassList.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 activeResizeObserveron 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.contentRectdoesn't include padding or border. If your breakpoints should account for those, useentry.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 loopwarnings. 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.