UI & Layout
useMeasure
Measures the width and height of a DOM element using ResizeObserver.
About
Measures both the inner and outer dimensions of any DOM element in a performant way and updates when dimensions change. Uses ResizeObserver for efficient size change detection, providing comprehensive measurement data including inner dimensions (clientWidth/clientHeight), outer dimensions (offsetWidth/offsetHeight), and scroll dimensions for both inner and outer measurements.
Examples
Basic usage
import React from "react";
import { useMeasure } from "rooks";
export default function App() {
const [ref, measurements] = useMeasure();
return (
<div
ref={ref}
style={{
width: 200,
height: 150,
border: "5px solid #ccc",
overflow: "auto",
padding: "10px",
margin: "20px"
}}
>
<div style={{ width: 300, height: 200 }}>
<h4>Inner Measurements:</h4>
<p>Width: {measurements.innerWidth}, Height: {measurements.innerHeight}</p>
<p>Scroll: {measurements.innerScrollWidth} x {measurements.innerScrollHeight}</p>
<h4>Outer Measurements:</h4>
<p>Width: {measurements.outerWidth}, Height: {measurements.outerHeight}</p>
<p>Scroll: {measurements.outerScrollWidth} x {measurements.outerScrollHeight}</p>
<p>This content is larger than the container to demonstrate scroll measurements.</p>
</div>
</div>
);
}With callback and debouncing
import React, { useState } from "react";
import { useMeasure } from "rooks";
export default function App() {
const [measurementHistory, setMeasurementHistory] = useState([]);
const [ref, measurements] = useMeasure({
debounce: 100,
onMeasure: (newMeasurements) => {
setMeasurementHistory(prev => [
...prev.slice(-4), // Keep last 5 measurements
newMeasurements
]);
}
});
return (
<div>
<textarea
ref={ref}
placeholder="Resize this textarea to see measurements change..."
style={{
width: "100%",
minHeight: 100,
resize: "both",
border: "3px solid #007acc",
padding: "8px"
}}
/>
<div>
<h3>Current Measurements:</h3>
<div style={{ display: "flex", gap: "20px" }}>
<div>
<h4>Inner:</h4>
<p>{measurements.innerWidth} x {measurements.innerHeight}</p>
<p>Scroll: {measurements.innerScrollWidth} x {measurements.innerScrollHeight}</p>
</div>
<div>
<h4>Outer:</h4>
<p>{measurements.outerWidth} x {measurements.outerHeight}</p>
<p>Scroll: {measurements.outerScrollWidth} x {measurements.outerScrollHeight}</p>
</div>
</div>
<h3>Measurement History:</h3>
{measurementHistory.map((measurement, index) => (
<div key={index}>
<small>
{index + 1}: Inner {measurement.innerWidth}x{measurement.innerHeight},
Outer {measurement.outerWidth}x{measurement.outerHeight}
</small>
</div>
))}
</div>
</div>
);
}Responsive component layout with inner/outer awareness
import React from "react";
import { useMeasure } from "rooks";
export default function ResponsiveCard() {
const [ref, measurements] = useMeasure();
// Adjust layout based on container size
const isCompact = measurements.innerWidth < 300;
const showDetails = measurements.innerHeight > 150;
const hasThickBorder = (measurements.outerWidth - measurements.innerWidth) > 10;
return (
<div
ref={ref}
style={{
width: "100%",
maxWidth: 400,
height: "100%",
maxHeight: 300,
border: hasThickBorder ? "8px solid #ddd" : "1px solid #ddd",
borderRadius: "8px",
padding: "16px",
display: "flex",
flexDirection: isCompact ? "column" : "row",
gap: "12px"
}}
>
<div style={{ flex: 1 }}>
<h3>Responsive Card</h3>
<p>Inner: {measurements.innerWidth}x{measurements.innerHeight}</p>
<p>Outer: {measurements.outerWidth}x{measurements.outerHeight}</p>
<p>Layout: {isCompact ? "Compact" : "Wide"}</p>
<p>Border: {hasThickBorder ? "Thick" : "Thin"}</p>
</div>
{showDetails && (
<div style={{
flex: isCompact ? "none" : 1,
backgroundColor: "#f5f5f5",
padding: "8px",
borderRadius: "4px"
}}>
<p>Additional details shown when height > 150px</p>
<small>
Border + padding: {measurements.outerWidth - measurements.innerWidth}px wide,
{measurements.outerHeight - measurements.innerHeight}px tall
</small>
</div>
)}
</div>
);
}Comparing inner vs outer measurements
import React from "react";
import { useMeasure } from "rooks";
export default function MeasurementComparison() {
const [ref, measurements] = useMeasure();
const borderWidth = (measurements.outerWidth - measurements.innerWidth) / 2;
const borderHeight = (measurements.outerHeight - measurements.innerHeight) / 2;
return (
<div style={{ padding: "20px" }}>
<div
ref={ref}
style={{
width: 300,
height: 200,
border: "15px solid #ff6b6b",
padding: "25px",
backgroundColor: "#4ecdc4",
overflow: "auto"
}}
>
<div style={{ width: 400, height: 250, backgroundColor: "#45b7d1" }}>
<h3>Measurement Breakdown</h3>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "10px" }}>
<div>
<h4>Inner (Content Area):</h4>
<p>Width: {measurements.innerWidth}px</p>
<p>Height: {measurements.innerHeight}px</p>
<p>Scroll Width: {measurements.innerScrollWidth}px</p>
<p>Scroll Height: {measurements.innerScrollHeight}px</p>
</div>
<div>
<h4>Outer (Including Border/Padding):</h4>
<p>Width: {measurements.outerWidth}px</p>
<p>Height: {measurements.outerHeight}px</p>
<p>Scroll Width: {measurements.outerScrollWidth}px</p>
<p>Scroll Height: {measurements.outerScrollHeight}px</p>
</div>
</div>
<div style={{ marginTop: "10px" }}>
<h4>Calculated Border + Padding:</h4>
<p>Horizontal: {borderWidth.toFixed(1)}px each side</p>
<p>Vertical: {borderHeight.toFixed(1)}px each side</p>
</div>
</div>
</div>
</div>
);
}Disabled measurements
import React, { useState } from "react";
import { useMeasure } from "rooks";
export default function ConditionalMeasurement() {
const [measurementEnabled, setMeasurementEnabled] = useState(true);
const [ref, measurements] = useMeasure({
disabled: !measurementEnabled
});
return (
<div>
<button onClick={() => setMeasurementEnabled(!measurementEnabled)}>
{measurementEnabled ? "Disable" : "Enable"} Measurements
</button>
<div
ref={ref}
style={{
width: 250,
height: 150,
border: "2px solid #007acc",
margin: "20px 0",
padding: "10px"
}}
>
<p>Measurements {measurementEnabled ? "enabled" : "disabled"}</p>
<div>
<h4>Inner:</h4>
<p>{measurements.innerWidth} x {measurements.innerHeight}</p>
<h4>Outer:</h4>
<p>{measurements.outerWidth} x {measurements.outerHeight}</p>
</div>
</div>
</div>
);
}Arguments
| Argument | Type | Description | Default value |
|---|---|---|---|
| options | UseMeasureOptions | Configuration options object | {} |
UseMeasureOptions
| Property | Type | Description | Default value |
|---|---|---|---|
| debounce | number | Number of milliseconds to debounce measurements | 0 |
| disabled | boolean | Whether measurements are disabled | false |
| onMeasure | (measurements: UseMeasureMeasurements) => void | Callback function called when dimensions change | undefined |
Return
Returns a tuple [ref, measurements] with the following structure:
| Index | Property | Type | Description |
|---|---|---|---|
| 0 | ref | CallbackRef | Callback ref to attach to the DOM element you want to measure |
| 1 | measurements | UseMeasureMeasurements | Object containing all measurement data |
UseMeasureMeasurements
| Property | Type | Description |
|---|---|---|
| innerWidth | number | Inner width (clientWidth) - content area width excluding scrollbars |
| innerHeight | number | Inner height (clientHeight) - content area height excluding scrollbars |
| innerScrollWidth | number | Total scrollable width (scrollWidth) - width of the entire content area |
| innerScrollHeight | number | Total scrollable height (scrollHeight) - height of the entire content area |
| outerWidth | number | Outer width (offsetWidth) - total element width including borders, padding, and scrollbars |
| outerHeight | number | Outer height (offsetHeight) - total element height including borders, padding, and scrollbars |
| outerScrollWidth | number | Total scrollable outer width - outer width of the entire scrollable content |
| outerScrollHeight | number | Total scrollable outer height - outer height of the entire scrollable content |
Notes
Browser Compatibility
- Requires ResizeObserver support (available in all modern browsers)
- Falls back gracefully in SSR environments
- Warns when ResizeObserver is not available
Performance Considerations
- Uses ResizeObserver for efficient resize detection instead of polling
- Supports debouncing to limit measurement frequency during rapid changes
- Automatically cleans up observers when components unmount
- Observes with multiple box types to track both inner and outer changes
Usage Tips
- The hook returns
0for all dimensions until a DOM element is attached via theref - Use the
disabledoption to temporarily stop measurements without losing the ref - The
onMeasurecallback is called after state updates, useful for logging or external state management - Debouncing is especially useful for expensive operations triggered by dimension changes
- Inner measurements exclude borders and padding, while outer measurements include them
- Use outer measurements when you need to know the total space an element occupies
- Use inner measurements when you need to know the available content area
Inner vs Outer Measurements
- Inner measurements (
innerWidth,innerHeight): Content area only, excludes borders, padding, and scrollbars - Outer measurements (
outerWidth,outerHeight): Complete element size including borders, padding, and scrollbars - Scroll measurements: Available for both inner and outer, showing total scrollable content dimensions
- Practical use: Outer measurements are useful for layout calculations, inner measurements for content positioning
SSR Compatibility
- Safe to use in server-side rendering environments
- Provides appropriate warnings when ResizeObserver or window is unavailable
- Returns default values (0) for all measurements during SSR