Blog

ResizeObserver – a new powerful tool for Responsive Web

09 Dec, 2019
Xebia Background Header Wave

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:

CSS Box model explained
Box model explained, source MDN

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:

Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts