react-use-measure
react-use-measure is a small React hook that observes one HTML or SVG element and returns its viewport-relative bounding rectangle. It combines ResizeObserver with window resize events and, optionally, captured scroll events from every scrollable ancestor, so width, height, x, y, top, right, bottom, and left update when layout moves. Options cover debouncing, an injected ResizeObserver polyfill, and unscaled HTML offset dimensions. The current return tuple also includes a manual refresh callback, even though the README's shorter signature still shows only ref and bounds.
A tiny, useful answer when reactive position and size are both required, especially inside scroll containers. Use a narrower ResizeObserver hook when width and height are all you need, and plan for zero first-render values.
Use it if
- A tooltip, overlay, chart, canvas, or pointer interaction needs reactive width, height, and viewport coordinates from one element
- The element can move because a nested scroll container moves, and observing scroll ancestors is worth the extra listeners
- You need the same hook for HTML and SVG elements and can tolerate zero bounds before the first client measurement
- A transformed layout needs offsetWidth and offsetHeight as an alternative to scaled bounding-box dimensions
- You need dimensions during the first render or on the server: the README says every bound is zero initially and real values arrive only after the view renders
- You only need width and height and already target modern browsers: a direct ResizeObserver hook can avoid scroll-container discovery, window listeners, and position state
- Legacy browser support must work with no extra dependency: the source throws when ResizeObserver is unavailable unless you inject a constructor such as @juggle/resize-observer
- Several systems must own the same ref without ref composition: the returned value is a callback ref used for unmount tracking, and the README directs multi-ref users to react-merge-refs
- You expect offsetSize to remove every transform effect: it replaces width and height with offset dimensions only for HTMLElement; x, y, edges, SVG elements, rotations, and ancestor transforms still follow getBoundingClientRect
Setup reality
Install react-use-measure with React 16.13 or newer; ReactDOM is an optional peer. The package has no runtime dependencies, includes ESM and CommonJS exports plus TypeScript declarations, and is about 1.3 KB gzipped. The hook is client-oriented but safe to render on the server: it substitutes an inert ResizeObserver class while window is absent, and initial bounds remain zeros. Hydration does not give you real geometry until the callback ref attaches and effects run, so do not branch server markup on measured width unless you accept a post-hydration layout change. Modern browsers provide ResizeObserver; older or unusual environments need an injected polyfill. The returned ref is a function, not a MutableRefObject despite the README's older API text. Attach it to exactly the element being measured. If another library needs that element, merge callback refs rather than replacing this one. The installed type declares a three-item tuple: ref, bounds, and forceRefresh. Measurements use getBoundingClientRect and therefore report viewport coordinates and transformed dimensions. offsetSize substitutes offsetWidth and offsetHeight for HTML elements only. Enable scroll:true when nested or page scrolling must update coordinates; the source then attaches passive capture listeners to every ancestor whose computed overflow is auto or scroll, plus window scroll, which can be noisy in deep layouts. Debounce resize and scroll independently for expensive children, understanding that visual overlays will lag during the delay. The hook always observes element resize and window resize, cleans listeners on unmount, and freezes each rectangle. In tests, jsdom generally needs a ResizeObserver stub plus explicit callback triggering; a rendered zero rectangle is normal until you simulate measurement. The latest release and push were January 2025, so verify React and browser regressions locally rather than assuming recent CI activity.
Patterns
Measure an element after layoutmeasure-element-bounds
import useMeasure from 'react-use-measure'
export function Card() {
const [measureRef, bounds] = useMeasure()
return (
<section ref={measureRef}>
{Math.round(bounds.width)} x {Math.round(bounds.height)}
</section>
)
}Every value is zero on the first render. The callback ref and observer schedule the real rectangle after the DOM node exists.
Size a chart from its containerrender-responsive-chart
const [containerRef, bounds] = useMeasure({ debounce: 50 })
return (
<div ref={containerRef} className="chart-shell">
{bounds.width > 0 && (
<Chart width={bounds.width} height={Math.max(240, bounds.width * 0.5)} />
)}
</div>
)Guard the zero-width first pass, and debounce only if chart rerenders are expensive enough to justify delayed resizing.
Update coordinates while ancestors scrolltrack-nested-scroll
const [targetRef, bounds] = useMeasure({
scroll: true,
debounce: { scroll: 8, resize: 80 },
})
return <div ref={targetRef}>Tracked target</div>scroll:true attaches captured passive listeners to scrollable ancestors and window. Avoid it for static layouts because frequent scroll updates rerender the owner.
Inject a ResizeObserver polyfillinject-resize-observer
import { ResizeObserver } from '@juggle/resize-observer'
import useMeasure from 'react-use-measure'
const [ref, bounds] = useMeasure({ polyfill: ResizeObserver })Install @juggle/resize-observer separately. Without a native or injected constructor, the hook throws in the browser.
Read unscaled HTML width and heightignore-scale-for-size
const [ref, bounds] = useMeasure({ offsetSize: true })
return (
<div style={{ transform: 'scale(0.75)' }}>
<div ref={ref}>Layout size: {bounds.width}</div>
</div>
)offsetSize uses offsetWidth and offsetHeight only for HTMLElement. Position and edge fields still come from getBoundingClientRect, and SVG size remains transformed.
Share the measured node with another refmerge-element-refs
import mergeRefs from 'react-merge-refs'
const localRef = useRef<HTMLDivElement>(null)
const [measureRef, bounds] = useMeasure()
return <div ref={mergeRefs([localRef, measureRef])}>{bounds.width}</div>react-merge-refs is a separate package. Replacing measureRef with localRef prevents the hook from observing and cleaning up the node.
Refresh after an external layout changeforce-manual-measurement
const [ref, bounds, forceRefresh] = useMeasure()
useEffect(() => {
const animation = document.fonts?.ready.then(forceRefresh)
return () => void animation
}, [forceRefresh])The third tuple item exists in the published types and source but is missing from the README signature. ResizeObserver often makes manual refresh unnecessary.
Position a fixed overlay from viewport boundsposition-overlay
const [anchorRef, bounds] = useMeasure({ scroll: true })
return (
<>
<button ref={anchorRef}>Details</button>
<div style={{ position: 'fixed', top: bounds.bottom + 8, left: bounds.left }}>
Overlay
</div>
</>
)Bounds are viewport-relative, which matches position:fixed. Portals, clipping, viewport edges, and focus handling still need an overlay library or application logic.
Convert pointer coordinates to element coordinatescalculate-local-pointer
const [surfaceRef, bounds] = useMeasure({ scroll: true })
function onPointerMove(event: React.PointerEvent) {
const local = {
x: event.clientX - bounds.left,
y: event.clientY - bounds.top,
}
updateCursor(local)
}
return <div ref={surfaceRef} onPointerMove={onPointerMove} />clientX and clientY are viewport-relative like getBoundingClientRect. CSS scale changes the measured coordinate system, so map again for an unscaled canvas model.
Observe an SVG elementmeasure-svg-element
const [svgRef, bounds] = useMeasure()
return (
<svg width="100%" height="240">
<g ref={svgRef}>
<circle cx="80" cy="80" r="40" />
</g>
<text y="220">{bounds.width.toFixed(1)}</text>
</svg>
)The callback accepts SVGElement, but offsetSize has no effect because offsetWidth and offsetHeight are HTMLElement-only.
Defer geometry-dependent UI until measuredavoid-hydration-branching
const [ref, bounds] = useMeasure()
const measured = bounds.width > 0 || bounds.height > 0
return (
<div ref={ref}>
<Content />
{measured && <GeometryDependentControls bounds={bounds} />}
</div>
)Server and first client render both see zeros. Keeping their stable content equal avoids using unknown geometry to produce mismatched markup.
Tune scroll and resize updates independentlydebounce-separately
const [ref, bounds] = useMeasure({
scroll: true,
debounce: {
scroll: 16,
resize: 120,
},
})Debouncing reduces renders but makes bounds temporarily stale. Overlays usually need a shorter scroll delay than expensive resize-driven charts.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| use-resize-observer | npm | You mainly need element width and height with rounding and ref options, not scroll-aware viewport coordinates |
| react-resize-detector | npm | You prefer a hook or component API with skip, refresh mode, and refresh-rate controls |
| @react-hook/resize-observer | npm | A minimal hook around ResizeObserver is enough and position tracking belongs elsewhere |