Skip to content
60fps/ui
@60fps/ui-utilsv0.0.2

ResizeObserver

Measure elements as they resize while every subscriber shares a single native ResizeObserver, with callbacks batched into one frame so nothing thrashes.

How it works

getResizeObserver() hands out a single shared ResizeObserver. When the browser reports resizes, the entries are buffered and flushed once inside a requestAnimationFrame, keeping only the latest entry per element — so each subscriber is notified at most once per element per frame instead of several times. That coalescing is what keeps many simultaneous observations off the resize-loop hot path.

The one option that changes the metrics, box, can’t be mixed on a single native observer, so the manager keeps one shared observer per box mode: content-box and border-box each get their own. That box is the only axis along which a second observer is ever created — everything else funnels through the same one.

Vanilla
0 × 0

Drag the bottom-right corner to resize ↘

import { getResizeObserver } from '@60fps/ui-utils';

// Every caller shares the same content-box observer.
const observer = getResizeObserver();
const onResize = (entry: ResizeObserverEntry) => {
	console.log(entry.contentRect.width, entry.contentRect.height);
};

observer.subscribe(el, onResize);
// later…
observer.unsubscribe(el, onResize);

getResizeObserver(options?)

MemberTypeDefaultDescription
subscribe(el, cb)(Element, cb) => voidStart observing an element; multiple callbacks per element are supported.
unsubscribe(el, cb)(Element, cb) => voidRemove one callback; the element is unobserved once its last callback is gone.
observerResizeObserverThe underlying shared observer instance.
options.box'content-box' | 'border-box''content-box'Which box metrics to observe. One shared observer is kept per box mode.

Convenience hooks

useResizeObserver wraps the manager with framework lifecycle — pass a ref and a callback, and it subscribes once the ref is attached and cleans up on unmount. The latest callback is always used, so an inline arrow is fine.

0 × 0

Drag the bottom-right corner to resize ↘

0 × 0

Drag the bottom-right corner to resize ↘

import { useRef, useState } from 'react';
import { useResizeObserver } from '@60fps/ui-react/resize-observer';

function Measured() {
	const ref = useRef<HTMLDivElement>(null);
	const [width, setWidth] = useState(0);

	useResizeObserver(ref, (entry) => setWidth(entry.contentRect.width));

	return (
		<div ref={ref} className="resize overflow-auto">
			{Math.round(width)}px
		</div>
	);
}
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue';
import { useResizeObserver } from '@60fps/ui-vue/resize-observer';

const box = useTemplateRef<HTMLElement>('box');
const width = ref(0);

useResizeObserver(box, (entry) => (width.value = entry.contentRect.width));
</script>

<template>
	<div ref="box" class="resize overflow-auto">{{ Math.round(width) }}px</div>
</template>

useResizeObserver(ref, callback)

ArgumentTypeDefaultDescription
refRefObject<Element> | Ref<Element>The element to observe. Re-subscribes if it changes.
callback(entry, observer) => voidCalled once per frame with the latest ResizeObserverEntry for the element.