ResizeObserver in WebKit

For years now, web developers have desired the ability to design components that are responsive to their container instead of the viewport. Developers are used to using media queries against viewport width for responsive designs, but having media queries based on element sizes is not possible in CSS because it could result in circular dependencies. Thus, a JavaScript solution was required.

ResizeObserver was introduced to solve this problem, allowing authors to observe changes to the layout size of elements. It was first made available in Chrome 64 in January 2018, and it’s now in Safari Technology Preview releases (and Epiphany Technology Preview). ResizeObserver was enabled by default as of Safari Technology Preview 97.

API Overview

A script creates a ResizeObserver with a callback which will be called with ‘observations’, and registers/unregisters callbacks using .observe(element), and .unobserve(element). Each call to observe(element) adds that element to the set of elements observed by this ResizeObserver instance.

The callback provided to the constructor is called with a collection of observerEntries which contain data about the state of CSS boxes being observed, if those boxes actually changed size. The observer itself also has a .disconnect() method which stops the active delivery of observed changes to the callback. Here’s a simple example:

const callback = (entries) => {
  console.log(`${entries.length} resize observations happened`)
  Array.from(entries).forEach((entry) => {
    let rect = entry.contentRect;
    console.log(
      entry.target,
      `size is now ${rect.width}w x ${rect.height}h`
    )
  })
}

const myObserver = new ResizeObserver(callback)

myObserver.observe(targetElementA)
myObserver.observe(targetElementB)

What we are observing with ResizeObserver is changes to the size of CSS Boxes that we have observed. Since we previously had no information on these boxes before observing, and now we do, this creates an observable effect. Assuming that targetElementA and targetElementB are in the DOM, we will see a log saying that 2 resize observations happened, and providing some information about the elements and sizes of each. It will look something like:

"2 resize observations happened"
"<div class='a'>a</div>" "size is now 1385w x 27h"
"<div class='b'>b</div>" "size is now 1385w x 27h"

Similarly, this means that while it is not an error to observe an element that isn’t in the DOM tree, no observations will occur until a box is actually laid out (when it is inserted, and creates a box). Removing an observed element from the DOM tree (which wasn’t hidden) also causes an observation.

How Observations are Delivered

ResizeObserver strictly specifies when and how things happen and attempts to ensure that calculation and observation always happen “downward” in the tree, and to help authors avoid circularity. Here’s how that happens:

  1. Boxes are created.
  2. Layout happens.
  3. The browser starts a rendering update, and runs the steps up to and including the Intersection Observer steps.
  4. The system gathers and compares the box sizes of observed element with their previously recorded size.
  5. ResizeObserver callback is called passing ResizeObserverEntry objects containing information about the new sizes.
  6. If any changes are incurred during the callback, then layout happens again, but here, the system finds the shallowest at which depth a change occurred (measured in simple node depth from the root). Any changes that are related to something deeper down in the tree are delivered at once, while any that are not are queued up and delivered in the next frame, and an error message will be sent to the Web Inspector console: (ResizeObserver loop completed with undelivered notifications).
  7. Subsequent steps in the rendering updates are executed (i.e. painting happens).

Note

In Safari Technology Preview, entries contain a .contentRect property reflecting the size of the Content Box. After early feedback, the spec is being iterated on in backward compatible ways which will also provide a way to get the measure of the Border Box. Future versions of this API will also allow an optional second argument to .observe which allows you to specify which boxes (Content or Border) you want to receive information about.

Useful Example

Suppose that we have a component containing an author’s profile. It might be used on devices with many sized screens, and in many layout contexts. It might even be provided for reuse as a custom element somehow. Further, these sizes can change at runtime for any number of reasons:

  • On a desktop, the user resizes their window
  • On a mobile device, the user changes their orientation
  • A new element comes into being, or is removed from the DOM tree causing a re-layout
  • Some other element in the DOM changes size for any reason (some elements are even user resizable)

Depending on the amount of space available to us at any given point in time, we’d like to apply some different CSS—laying things out differently, changing some font sizes, perhaps even using different colors.

For this, let’s assume that we follow a ‘responsive first’ philosophy and make our initial design for the smallest screen size. As available space gets bigger, we have another design that should take effect when there are 768px available, and still another when there are at least 1024px. We’ll make these designs with our page using classes “.container-medium” and “.container-large”. Now all we have to do is add or remove those classes automatically.

/* Tell the observer how to manage the attributes */
const callback = (entries) => {
  entries.forEach((entry) => {
    let w = entry.contentRect.width
    let container = entry.target

    // clear out any old ones
    container.classList.remove('container-medium', 'container-large')

    // add one if a 'breakpoint' is true
    if (w > 1024) {
      container.classList.add('container-large')
    } else if (w > 768) {
      container.classList.add('container-medium')
    }
  }) 
}

/* Create the instance **/
const myObserver = new ResizeObserver(callback)

/* Find the elements to observe */
const profileEls = [...document.querySelectorAll('.profile')]

/* .observe each **/
profileEls.forEach(el => myObserver.observe(el))

Now, each .profile element will gain the class of .container-medium or .container-large if their available size meets our specified criteria, and our designs will always be appropriately applied based on their available size. You can, of course, combine this with a MutationObserver or as a Custom Element in order to account for elements which might come into existence later.

Feedback

We’re excited to have ResizeObserver available in Safari Technology Preview! Please try it out and file bugs for any issues you run into.