ResizeObserver
lets you know when an element's size changes.
Before ResizeObserver
, you had to attach a listener to the document's resize
event to get notified of any change of the viewport's dimensions. In the event
handler, you would then have to figure out which elements have been affected by
that change and call a specific routine to react appropriately. If you needed
the new dimensions of an element after a resize, you had to call
getBoundingClientRect()
or getComputedStyle()
, which can cause layout
thrashing if you don't take care of batching all your reads and all your
writes.
This didn't even cover cases where elements change their size without the main
window having been resized. For example, appending new children, setting an
element's display
style to none
, or similar actions can change the size of
an element, its siblings, or its ancestors.
This is why ResizeObserver
is a useful primitive. It reacts to changes in
size of any of the observed elements, independent of what caused the change.
It provides access to the new size of the observed elements too.
API
All the APIs with the Observer
suffix we mentioned above share a simple API
design. ResizeObserver
is no exception. You create a ResizeObserver
object
and pass a callback to the constructor. The callback is passed an array of
ResizeObserverEntry
objects—one entry per observed element—which
contains the new dimensions for the element.
var ro = new ResizeObserver(entries => {
for (let entry of entries) {
const cr = entry.contentRect;
console.log('Element:', entry.target);
console.log(`Element size: ${cr.width}px x ${cr.height}px`);
console.log(`Element padding: ${cr.top}px ; ${cr.left}px`);
}
});
// Observe one or multiple elements
ro.observe(someElement);
Some details
What is being reported?
Generally, a
ResizeObserverEntry
reports the content box of an element through a property called
contentRect
, which returns a
DOMRectReadOnly
object. The content box is the box in which content can be placed. It is
the border box minus the padding.
It's important to note that while ResizeObserver
reports both the dimensions
of the contentRect
and the padding, it only watches the contentRect
.
Don't confuse contentRect
with the bounding box of the element. The bounding
box, as reported by getBoundingClientRect()
, is the box that contains the
entire element and its descendants. SVGs are an exception to the rule, where
ResizeObserver
will report the dimensions of the bounding box.
As of Chrome 84, ResizeObserverEntry
has three new properties to provide more
detailed information. Each of these properties returns a ResizeObserverSize
object containing a blockSize
property and an inlineSize
property. This
information is about the observered element at the time the callback is invoked.
borderBoxSize
contentBoxSize
devicePixelContentBoxSize
All of these items return read-only arrays because in the future it's hoped that they can support elements that have multiple fragments, which occur in multi-column scenarios. For now, these arrays will only contain one element.
Platform support for these properties is limited, but Firefox already supports the first two.
When is it being reported?
The spec proscribes that ResizeObserver
should process all resize events
before paint and after layout. This makes the callback of a ResizeObserver
the
ideal place to make changes to your page's layout. Because ResizeObserver
processing happens between layout and paint, doing so will only invalidate
layout, not paint.
Gotcha
You might be asking yourself: what happens if I change the size of an observed
element inside the callback to ResizeObserver
? The answer is: you will trigger
another call to the callback right away. Fortunately, ResizeObserver
has a
mechanism to avoid infinite callback loops and cyclic dependencies. Changes will
only be processed in the same frame if the resized element is deeper in the DOM
tree than the shallowest element processed in the previous callback.
Otherwise, they'll get deferred to the next frame.
Application
One thing that ResizeObserver
allows you to do is to implement per-element
media queries. By observing elements, you can imperatively define your
design breakpoints and change an element's styles. In the following
example, the second box
will change its border radius according to its width.
const ro = new ResizeObserver(entries => {
for (let entry of entries) {
entry.target.style.borderRadius =
Math.max(0, 250 - entry.contentRect.width) + 'px';
}
});
// Only observe the second box
ro.observe(document.querySelector('.box:nth-child(2)'));
Another interesting example to look at is a chat window. The problem that arises in a typical top-to-bottom conversation layout is scroll positioning. To avoid confusing the user, it is helpful if the window sticks to the bottom of the conversation, where the newest messages appear. Additionally, any kind of layout change (think of a phone going from landscape to portrait or vice versa) should achieve the same.
ResizeObserver
allows you to write a single piece of code that takes care of
both scenarios. Resizing the window is an event that a ResizeObserver
can
capture by definition, but calling appendChild()
also resizes that element
(unlessoverflow: hidden
is set), because it needs to make space for the new
elements. With this in mind, it takes very few lines to achieve the desired
effect:
const ro = new ResizeObserver(entries => {
document.scrollingElement.scrollTop =
document.scrollingElement.scrollHeight;
});
// Observe the scrollingElement for when the window gets resized
ro.observe(document.scrollingElement);
// Observe the timeline to process new messages
ro.observe(timeline);
Pretty neat, huh?
From here, I could add more code to handle the case where the user has scrolled up manually and wants scrolling to stick to that message when a new message comes in.
Another use case is for any kind of custom element that is doing its own layout.
Until ResizeObserver
, there was no reliable way to get notified when its
dimensions change so its children can be laid out again.
Effects on Interaction to Next Paint (INP)
Interaction to Next Paint (INP) is a metric that measures the overall responsiveness of a page to user interactions. If a page's INP is in the "good" threshold—that is, 200 milliseconds or less—it can said that a page is reliably responsive to the user's interactions with it.
While the amount of time it takes for event callbacks to run in response to a user interaction can contribute significantly to an interaction's total latency, that's not the only aspect of INP to consider. INP also considers the amount of time it takes for the next paint of the interaction to occur. This is the amount of time it takes for the rendering work required to update the user interface in response to an interaction to complete.
Where ResizeObserver
is concerned, this is important because the callback that
an ResizerObserver
instance runs occurs just prior to rendering work. This
is by design, as the work that occurs in the callback has to be taken into
account, as the result of that work will very likely require a change to the
user interface.
Take care to do as little rendering work as required in a ResizeObserver
callback, as excessive rendering work can create situations where the browser
is delayed in doing important work. For example, if any interaction has a
callback that causes a ResizeObserver
callback to run, ensure you're doing the
following to facilitate the smoothest possible experience:
- Ensure your CSS selectors are as simple as possible in order to avoid excessive style recalculation work. Style recalculations occur just prior to layout, and complex CSS selectors can delay layout operations.
- Avoid doing any work in your
ResizeObserver
callback that can triggers forced reflows. - The time required to update a page's layout generally increases with the
number of DOM elements on a page. While this is true whether or not pages use
ResizeObserver
, the work done in aResizeObserver
callback can become significant as a page's structural complexity increases.
Conclusion
ResizeObserver
is available in all major
browsers
and provides an efficient way to monitor for element resizes at an element
level. Just be cautious not to delay rendering too much with this powerful API.