Implementing Resize Observer for Adaptive Components on a Website
ResizeObserver watches for size changes of a specific DOM element. Not the viewport, but the element itself. This is the key difference from CSS media queries — here adaptivity is tied to the component's container, not the screen width.
Classic case: a chart component takes up the full page width, then one-third. CSS media query doesn't see this. ResizeObserver — sees it.
Basic Usage
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
// entry.contentRect — content area dimensions
// entry.borderBoxSize — dimensions including border
// entry.contentBoxSize — without padding/border
// entry.devicePixelContentBoxSize — in physical pixels
const { width, height } = entry.contentRect
console.log(`${entry.target.id}: ${width}x${height}`)
}
})
observer.observe(element)
observer.unobserve(element)
observer.disconnect()
Container Queries via JavaScript Before Native Support
Native CSS Container Queries (@container) are supported in modern browsers, but for older ones or complex logic — ResizeObserver:
function applyContainerBreakpoints(
element: HTMLElement,
breakpoints: Record<number, string>
): () => void {
const sortedBreakpoints = Object.entries(breakpoints)
.map(([w, cls]) => [Number(w), cls] as [number, string])
.sort(([a], [b]) => a - b)
const observer = new ResizeObserver(([entry]) => {
const width = entry.contentRect.width
// Remove all breakpoint classes
sortedBreakpoints.forEach(([, cls]) => element.classList.remove(cls))
// Add the appropriate one
for (const [minWidth, cls] of sortedBreakpoints) {
if (width >= minWidth) element.classList.add(cls)
}
})
observer.observe(element)
return () => observer.disconnect()
}
// Usage:
applyContainerBreakpoints(cardEl, {
0: 'card--xs',
320: 'card--sm',
480: 'card--md',
640: 'card--lg',
})
Automatic Canvas Resize
function makeResponsiveCanvas(
canvas: HTMLCanvasElement,
draw: (ctx: CanvasRenderingContext2D, width: number, height: number) => void
): () => void {
const ctx = canvas.getContext('2d')!
const dpr = window.devicePixelRatio || 1
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect
// Set actual pixel size
canvas.width = Math.round(width * dpr)
canvas.height = Math.round(height * dpr)
// CSS size remains the same
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
// Scale context for retina
ctx.scale(dpr, dpr)
draw(ctx, width, height)
})
observer.observe(canvas.parentElement ?? canvas)
return () => observer.disconnect()
}
React Hook
function useResizeObserver<T extends HTMLElement = HTMLDivElement>(): [
RefObject<T | null>,
DOMRectReadOnly | null,
] {
const ref = useRef<T>(null)
const [rect, setRect] = useState<DOMRectReadOnly | null>(null)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new ResizeObserver(([entry]) => {
setRect(entry.contentRect)
})
observer.observe(el)
return () => observer.disconnect()
}, [])
return [ref, rect]
}
// Adaptive component based on hook:
function AdaptiveChart({ data }: { data: number[] }) {
const [ref, rect] = useResizeObserver<HTMLDivElement>()
const isCompact = (rect?.width ?? 0) < 400
return (
<div ref={ref} style={{ width: '100%' }}>
{isCompact ? (
<CompactChart data={data} width={rect?.width} />
) : (
<FullChart data={data} width={rect?.width} height={rect?.height} />
)}
</div>
)
}
Debounce for Frequent Changes
ResizeObserver fires on every size change — on window resize this can happen several times per second. For heavy computations you need debounce:
function useResizeObserverDebounced<T extends HTMLElement>(
delay = 150
): [RefObject<T | null>, DOMRectReadOnly | null] {
const ref = useRef<T>(null)
const [rect, setRect] = useState<DOMRectReadOnly | null>(null)
const timerRef = useRef<ReturnType<typeof setTimeout>>()
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new ResizeObserver(([entry]) => {
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => setRect(entry.contentRect), delay)
})
observer.observe(el)
return () => {
observer.disconnect()
clearTimeout(timerRef.current)
}
}, [delay])
return [ref, rect]
}
What's Included
Setting up ResizeObserver for needed components, React hooks with debounce, adapting components by container width (not viewport), automatic canvas resize when needed.
Timeline: 0.5 day.







