Topic: Web platform
ResizeObserver basics: element sizes, boxes, and update boundaries
An element can shrink without the viewport changing. Use a runnable JavaScript example to understand ResizeObserver, content and border boxes, logical axes, hidden elements, cleanup, and resize feedback loops.
Animated meme (expand/collapse)
A sidebar opens and squeezes a chart. The browser window stays the same size, but the chart needs to redraw.
A listener for window.resize alone misses that requirement. ResizeObserver watches an element’s layout size and notifies your code when it changes. A resizable panel, a new Grid allocation, or wrapping content can change that size without a viewport resize. MDN: ResizeObserver
Before adding an observer, I decide which measurement the component needs and how it will use that measurement. A chart needs drawable space in its container. A card that only switches its layout may need a CSS container query instead. The receiving code still owns layout and drawing.
A panel you can run
Put this HTML in an empty page and the JavaScript after it. On desktop, drag the panel’s bottom-right handle. On devices where that is awkward, change its width in developer tools.
<div
id="resize-panel"
style="box-sizing: border-box; width: 300px; min-width: 160px;
max-width: 100%; padding: 12px; border: 2px solid;
resize: horizontal; overflow: auto;"
>
<p>Resize this panel. Its text can wrap.</p>
</div>
<p id="size-output" role="status">Waiting for a size notification.</p>
const panel = document.querySelector("#resize-panel");
const output = document.querySelector("#size-output");
let resizeObserver;
if ("ResizeObserver" in window) {
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target !== panel) continue;
const size = entry.borderBoxSize?.[0];
if (!size) {
output.textContent = "Border box data is unavailable.";
continue;
}
output.textContent =
`Border box: inline ${size.inlineSize.toFixed(1)}px, ` +
`block ${size.blockSize.toFixed(1)}px.`;
}
});
resizeObserver.observe(panel, { box: "border-box" });
} else {
output.textContent = "Live size reporting is unavailable.";
}
function stopWatchingPanel() {
resizeObserver?.disconnect();
}
This assumes the HTML exists and both queries return elements. Running before the component mounts can produce null. Wait for the element to exist; observe() does not search for a selector string.
The output sits outside the panel to reduce the chance that displaying a measurement changes the measured panel. The example reports data without a chart library or polling. Call stopWatchingPanel() when the application removes this demo.
entries lists the targets in this notification. One observer can watch several elements. A target missing from a batch has not necessarily been removed. Use entry.target to associate each measurement with its component. MDN: ResizeObserverEntry
What width includes determines the calculation
For an ordinary element, the CSS box model goes outward in this order:
content → padding → border → margin
The content box covers the content area. The border box includes content, padding, and borders, but not margins. ResizeObserver defaults to observing content-box changes. Request { box: "border-box" } when changes to the whole border box matter. MDN: observe() box option
CSS box-sizing uses the same names, which can be confusing. It controls how CSS width and height are calculated. The observer’s box selects what to observe. Calling observe(panel, { box: "border-box" }) does not change the element’s CSS box-sizing.
“The width is 180px” is incomplete information. Assume ordinary boxes without scrollbars or other size constraints:
| CSS settings | What does the specified width cover? | Calculation |
|---|---|---|
border-box; width: 180px, 8px padding and 1px border on each side |
The whole border box is 180px | Content width is 180 − 16 − 2 = 162px |
content-box; width: 150px, 12px padding and 3px border on each side |
The content area is 150px | Border-box width is 150 + 24 + 6 = 180px |
Identify the box the known number describes, then decide whether to add or subtract. A rule like “add padding to width” fails when border-box already includes it. MDN: box-sizing
This affects notifications too. Increasing padding while keeping the content box fixed can enlarge the border box without changing the content box. Default observation does not cover every padding change when the product cares about the outer size.
Choose the box, then the axis
| Field | Box | Axes |
|---|---|---|
entry.contentBoxSize[0] |
Content box | Logical inlineSize and blockSize |
entry.borderBoxSize[0] |
Border box | Logical inlineSize and blockSize |
entry.contentRect |
Content area of an ordinary HTML element | Physical width and height |
These examples use ordinary HTML elements with one fragment. Modern size fields are arrays: take [0], then read the size object. This differs from the callback’s entries. One describes size fragments for a target; the other lists targets in a notification. Fragmented multi-column layout is outside this article’s scope. MDN: contentBoxSize
Inline follows text along a line. Block follows the progression from one line to the next. Their physical directions depend on writing-mode:
| Writing mode | inlineSize corresponds to | blockSize corresponds to |
|---|---|---|
horizontal-tb |
Physical width | Physical height |
vertical-rl or vertical-lr |
Physical height | Physical width |
direction: rtl reverses horizontal text direction; it does not turn horizontal writing into vertical writing. A horizontal content area that is 240px wide and 70px tall still has inline size 240 and block size 70 in RTL. Lengths do not become negative. MDN: Different text directions
For “the border box’s block-axis length,” read:
const blockLength = entry.borderBoxSize[0].blockSize;
contentRect.height changes both the box and the axis. It is the content area’s physical height. With horizontal writing and no padding or border, the numbers might coincide. That does not make them interchangeable. MDN: borderBoxSize, contentRect
display: none and transform affect different things
transform: scale(0.5) scales the rendered result. It does not turn a 200px content box into a 100px content box. The CSSWG specification states that CSS transforms do not trigger resize observations. The observer is not a tracker for visual scaling or position.
Setting display: none on a rendered element with nonzero dimensions removes its previous layout box and produces an observable size change. It does not automatically disconnect() the observer. Hiding a component and stopping observation are separate actions. CSSWG: Notification conditions
visibility: hidden and opacity: 0 generally retain layout space. Invisible does not always mean zero-sized; visibility’s special collapse cases have separate rules. MDN: visibility, opacity
For a chart in a hidden tab, skip size-dependent drawing when the container measures zero and draw when space becomes available. This is an application policy. The API does not decide what a zero-width chart should display.
Can the size update stop?
ResizeObserver notifications participate in the browser’s rendering process. Changing layout in a callback can produce another size change. Consider increasing the observed element’s width on every notification:
entry.target.style.width = `${entry.contentRect.width + 10}px`;
Assuming content-box sizing and no other width constraints, the value keeps growing. The browser limits cycles within a rendering update and may report ResizeObserver loop completed with undelivered notifications. It does not repair the update rule. MDN: Observation errors
Animated meme (expand/collapse)
A stopping condition changes the content width to 320px only when it is smaller:
if (entry.contentRect.width < 320) {
entry.target.style.width = "320px";
}
This still assumes content-box sizing and no rule preventing the width from reaching 320px. Once it reaches the target, the code stops writing. That provides a stable value, not a promise of exactly one notification. If another function shrinks it again, inspect that function too.
For a chart, a common arrangement observes a container whose size comes from the surrounding layout, then updates an inner canvas. But if the canvas determines the container’s size, observing the parent alone may not help. Follow the size dependencies, including updates across elements and observers.
requestAnimationFrame() can combine expensive drawing and retain the latest size. It does not prove convergence. Moving “add 10px every time” into the next frame can still make the element grow indefinitely. Callbacks also run on the main thread; asynchronous notification does not make expensive work free. web.dev: Usage and performance
Initial notifications, content changes, and cleanup
A rendered element with nonzero dimensions normally receives an initial notification when observation starts. That is not a signal that every image and font has loaded. Exporting a final chart requires separate conditions for data and resource readiness. CSSWG: Starting observation
Adding a message to a fixed-size chat panel may only increase its scrollable content, leaving its observed box unchanged. Handle the message in the application’s insertion flow. Consider MutationObserver when DOM changes are the requirement. ResizeObserver does not send an event for every content edit.
Cleanup should match ownership:
unobserve(target)stops one target, useful when one component sharing an observer unmounts.disconnect()stops all targets for that observer. The object can later start new observations withobserve().
Neither method deletes DOM or cancels drawing and requests already scheduled by a callback. If you schedule rAF or fetch work, clean up that work separately. MDN: unobserve(), disconnect()
When CSS is enough
If a card only needs to switch between one and two columns based on its panel’s width, start with a CSS container query. Set container-type: inline-size on a suitable ancestor and use @container for child styles. An observer does not need to maintain a class for that job. MDN: Container queries
ResizeObserver fits when JavaScript drawing, a chart, or an existing layout function needs measurements. Size changes, content changes, and intersection state are separate requirements with different tools.
Check compatibility for the fields and options you use. The first example reports unavailable functionality when border-box data is missing and preserves the ordinary panel. A fallback must preserve box and axis semantics if the product requires equivalent measurement. Renaming contentRect.width to border width does not do that. Verify older data shapes and feature support in the target browsers.
What I learned
- I identify what CSS width includes before calculating content and border sizes. Observing the border box does not change CSS box-sizing.
- I choose the box and axis separately. RTL is not vertical writing, and blockSize is not always height.
- I distinguish layout boxes from visual effects. A hidden tab and a scaled element need different measurement handling.
- I check that size updates can stop before combining drawing work. Stopping an observer and canceling other work need separate cleanup.
To practice, resize the panel and then change padding, writing-mode, display, and transform one at a time. Explain why the notification and values differ before connecting the observer to a chart.
References and further learning
- MDN ResizeObserver: API, compatibility, and observation-loop guidance.
- MDN CSS box model: diagrams of content, padding, borders, and margins.
- web.dev ResizeObserver: element measurements, drawing, and performance tradeoffs.
- Web Dev Simplified introduction: the author’s 2022 introduction. Check current documentation for compatibility and CSS capabilities.
- Learn Resize Observer In 5 Minutes: the same author’s public introductory video, suitable for practicing in an empty page.