The word “responsive” is something we don’t mention that often these days in web development, it’s a standard already. There is a vast and ever-growing variety of screens. We want to be able to support all the possible sizes and still keep a good user experience. And CSS media-queries are a great solution to this challenge. But how about responsive components? Modern web development is about components and we need a way to make them responsive as well. Today I want to talk about ResizeObserver API, a new powerful tool for Responsive Web, which in contrast to media-queries, allows detecting a size change of a particular element rather than a whole viewport.
A brief history
In the past we only had media queries
— CSS solution base on a media size, media type or media screen resolution (by media, we mean desktop, phone or tablet). Media queries are flexible and easy to use. For a long time, this solution was only available for CSS, until it became accessible also in JS via window.matchMedia(<em>mediaQueryString</em>)
. Now we can see if the document matches the media and even more — observe a document to detect when its media queries change.
Element queries, how about them?
What we still miss in our toolbox is a way to detect size changes on a single DOM element, not a viewport. Developers have been talking about this tool for years already. It’s one of the most requested features. And there is even a proposal from 2015 — container queries:
Given a complex responsive layout, developers often require granular control over styling elements relative to the size of their parent container rather than the viewport size. Container queries allow an author to control styling based on the size of a containing element rather than the size of the user’s viewport. Example of usage in CSS:
.element:media(min-width: 30em) screen {***}
Sounds great, but unfortunately, browsers had a reason to hesitate with an implementation — circular dependency (when one resize event triggers another, leading to infinite loop; click here to read more), so this specification never turned into a W3C draft. So what are the options? We could use window.resize(callback)
, but it’s an expensive solution — the callback is called each time the event is triggered and we still need a lot of calculations to detect if our component has actually changed its size…
Observing changes to element’s size with ResizeObserver API
Luckily, the Chrome team is coming to the rescue — welcome ResizeObserver API:
The ResizeObserver API is an interface for observing changes to element’s size. It is an element’s counterpart to
window.resize
event.
The ResizeObserver API is a living draft. It already works in Chrome 64, Firefox and Safari for desktop. Mobile support is less charming — only Chrome on Android and Samsung Internet support it. There’s more bad news: it’s not 100% polyfillable. Available polyfills have usually limitations (like slow response, no support for delayed transition, etc), so be aware. Anyway, it should not stop us from seeing it in action, so let’s do that!
Example: change the text of an element base on its size
Let’s consider the following situation — text inside of an element should change based on the size of an element. ResizeObserver API gives us two interfaces — ResizeObserver
and ResizeObserverEntry
. ResizeObserver
interface is used to observe changes to an element’s size and ResizeObserverEntry
describes actually an element that has been resized. Code is very straightforward (the clickable version here):
<h1> 😊😊😊 </h1>
<h2> boring text </h2>
var ro = new ResizeObserver( entries => {
for (let entry of entries) {
const width = entry.contentBoxSize ? entry.contentBoxSize.inlineSize : entry.contentRect.width;
if (entry.target.tagName === 'H1') {
entry.target.textContent = width < 1000 ? '😱😱😱' : '😊😊😊';
}
if (entry.target.tagName === 'H2' && width < 500) {
entry.target.textContent = 'I won"t change anymore';
ro.unobserve(entry.target); // stop observing this element when it's size will reach 500px
}
}
});
// we can add more than one element to observe
ro.observe(document.querySelector("h1"));
ro.observe(document.querySelector("h2"));
We have created a ResizeObserver
object and passed a callback to its constructor:
const resizeObserver = new ResizeObserver((entries, observer) => {
for (let entry of entries) {
// check entry dimensions and perform needed logic
}
})
This function is called whenever one of the target elements, ResizeObserverEntries
, will change its size. The second parameter of the callback function is an observer itself. You can use it, for example, to stop observing an element when a certain condition has been reached.
The callback receives an array of ResizeObserverEntry
. Each entry contains new dimensions for the observed element and element itself (target
).
for (let entry of entries) {
const width = entry.contentBoxSize ? entry.contentBoxSize.inlineSize : entry.contentRect.width;
if (entry.target.tagName === 'H1') {
entry.target.textContent = width < 1000 ? '😱😱😱' : '😊😊😊';
}
...
}
There are three properties describing new dimensions of the element — borderBoxSize
, contentBoxSize
and contentRect
. They represent a box model of an element, we will dive into that soon. For now a few words about support. I mainly see the usage of the contentRect
property, as it has the best support among the browsers, but be aware that this property may be deprecated:
contentRect
is from the incubation phase of ResizeObserver and is only included for current web compat reasons. It may be deprecated in future levels.
So I would advise using borderBoxSize
or contentBoxSize
with a fallback to contentRect
. ResizeObserverSize interface provides two properties: inlineSize
and blockSize
, which you can read as width
and height
(assuming we work in horizontal writing-mode
).
Observing an element
The last thing to do is to actually start observing an element. To do that we call ResizeObserver.observe()
, which adds our element as a new target to the list of observed elements. We can pass either one element or multiple:
// resizeObserver.observe(target, options);
ro.observe(document.querySelector("h1"));
ro.observe(document.querySelector("h2"));
The second parameter, options
, is optional. Currently, the only option available is box
which determines the box model we want observe changes to. Possible values are content-box
(default), border-box
and device-pixel-content-box
(which is chrome only). You can only observe one box per observer, so you will need to use multiple ResizeObservers if you want to observe multiple boxes for the same element.
In order to stop observing a particular element, we can use ResizeObserver.unobserve(target)
. To stop observing all the targets, use ResizeObserver.disconnect()
.
Difference between box models
The content box is the box with the content, without paddings, border and margins. The border box, in contrast, includes paddings and border width, but still no margins:
Device pixels content box is a content box of an element in device pixels. I haven’t seen the usage of this option yet, but it seems it can be useful for a canvas element. There is an interesting discussion on a Github about it if you want to understand more where and why it can be used.
When does an observer notify about changes?
The callback fires whenever the target element changes its size. The specification has the following information about this:
- Observation will fire when watched element is inserted/removed from DOM;
- Observation will fire when watched element display gets set to none;
- Observations do not fire for non-replaced inline elements;
- Observations will not be triggered by CSS transforms;
- Observation will fire when observation starts if element is being rendered, and element’s size is not 0,0.
Looking at the first point, now we can detect the change of the parent container size when its children have changed. A nice example of this is scrolling to the bottom of the chat window when new message is added. With ResizeObsever it’s very easy! See an example here.
Now, remember the Element Queries proposal I mentioned earlier? And its circular dependency issue? The ResizeObserver API has a built-in solution to prevent infinite resize loops. Click here to read all about it.
Alright, I hope you are excited about working with ResizeObserver API! Thank you for staying with me until these words ❤ Have fun and happy coding!
Useful links:
- Specification;
- MDN documentation of ResizeObserver API.
- CanIUse;
- The first article/announcement from Chrome team;
- Most reliable Polyfill;
- Element queries proposal explained.