CSS animations for DOM observation
Tags: css, webdev
Published: 2020-02-13 17:51:33

---
title: CSS animations for DOM observation
published: true
date: 2020-02-11 14:00:15 UTC
tags: css, webdev
canonical_url: https://blog.logrocket.com/css-animations-for-dom-observation/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/g2xhfd94vxux7dldrhpk.jpeg
---
**Written by [Glad China](https://blog.logrocket.com/author/gladchinda/)**✏️

Web developers are often tasked with building applications that can adapt to a plethora of changes, usage conditions, and user actions.

Simply put, the appearance and behavior of a web application at any given time is a function of many variables: application state, device viewport, device capabilities, network conditions, and user preferences, to mention a few. In order to keep track of these variables and react to changes, some form of observation and detection will be required.

Observation has been an integral part of web development for a long time. You might already know Web APIs like `IntersectionObserver`, `MutationObserver`, `PerformanceObserver`, etc. However, it is easy to overlook that event listeners are also observers themselves. As a matter of fact, each time you set up an event listener for a particular event on one or more event targets, you’ve just got yourself an observer.

That said, here is a question for us:

> What if we are interested in observing and reacting to some form of changes in the DOM like node insertions, node removals, attribute changes, etc.?

Someone might say, “Well, that will not be necessary.” Another might scream, “`MutationObserver`!”

But what if I said CSS animations? Think about it for a moment.

In this article, we will explore how we can leverage CSS animations for the purpose of observing changes in the DOM, along with the kinds of changes that can be observed, using practical examples.

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## Background

Imagine working on a project where you are required to build a script that developers can add to their web application to embed certain elements like iframes, buttons, widgets, etc. at different parts of the application.

Let’s say, for example, that for every element with the `.share-button` class, you are required to replace it with a special kind of button for sharing some content on some platform.

You could quickly do something like this:

```jsx
document.addEventListener('DOMContentLoaded', () => {
  replaceWithShareButton(document.querySelectorAll('.share-button'));
});

function replaceWithShareButton (nodes) {
  Array.from(nodes).forEach(node => {
    const button = document.createElement('button');

    // set button attributes and properties here

    requestAnimationFrame(() => {
      node.parentNode.replaceChild(button, node);
    });
  });
}
```

This is perfect as long as all the `.share-button` elements already exist as part of the page markup when it is loaded — for example, if the page was server-side rendered. However, when more `.share-button` elements get added to the page dynamically, nothing happens.

To handle this new scenario, you decide to leverage the `MutationObserver` API to watch the DOM tree for new `.share-button` elements being added, and effectively replace them with the special button.

The following code snippet shows some modifications you could make to the `DOMContentLoaded` event listener from earlier in order to set up the mutation observer.

```jsx
document.addEventListener('DOMContentLoaded', () => {
  replaceWithShareButton(document.querySelectorAll('.share-button'));

  const observer = new MutationObserver(mutations => {
    for (let mutation of mutations) {
      const nodes = Array.from(mutation.addedNodes)
        .filter(node => node.classList.contains('share-button'));

      replaceWithShareButton(nodes);
    }
  });

  observer.observe(document.body, { childList: true, subtree: true });
});
```

Thanks to the awesome `MutationObserver` API, you’ve succeeded in getting the button replacements to work perfectly with very little effort.

A couple days later, the project manager comes to you and says he’s been receiving complaints from developers saying that the button replacements aren’t happening on some browsers, and you are required to quickly fix it.

After doing some research, you soon realize that the issue was because the `MutationObserver` API is not supported by some browsers that still happen to be in use.

![MutationObserver API Browser Support](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/mutationobserver-api-browser-support.png?resize=730%2C223&ssl=1)<figcaption id="caption-attachment-13740">Browser support for the MutationObserver API, via Can I use…</figcaption>

The question now is: What other options do you have, while still ensuring that you support some more browsers?

Of course, you can try the long polling technique, where you check at intervals for `.share-button` elements that have just been added and replace them. So, you modify the `DOMContentLoaded` event listener again to use long polling, like so:

```jsx
document.addEventListener('DOMContentLoaded', () => {
  replaceWithShareButton(document.querySelectorAll('.share-button'));

  const POLLING_INTERVAL = 2000;

  setTimeout(function replaceNewerNodes () {
    replaceWithShareButton(document.querySelectorAll('.share-button'));
    setTimeout(replaceNewerNodes, POLLING_INTERVAL);
  }, POLLING_INTERVAL);
});
```

While using this technique works on a wider range of browsers (including the pretty old ones), it is very clear that it will have some performance issues, especially with `setTimeout()` being called frequently. Also, coupled with the fact that the button replacements will not happen in real time due to the polling interval, you decide not to go with this technique.

So you are back to looking for alternatives.

Don’t you think you should try bringing CSS animation into the mix? Let’s see how that can help solve your little problem.

## Basic principle

A couple years ago, David Walsh wrote on his blog about a technique for [detecting DOM node insertions with JavaScript and CSS animations](https://davidwalsh.name/detect-node-insertion), which he said was introduced to him by [Daniel Buchner](http://www.backalleycoder.com/), a Mozilla developer at the time.

The basic principle of this technique is as follows:

- CSS animation is added to the target elements to be observed
- CSS animation starts playing when those elements get added to the DOM
- The `animationstart` event is fired whenever a CSS animation starts playing

Hence, to solve the problem you encountered earlier, you will have to first set up the following styles on the page:

```jsx
.share-button {
  animation-name: button-inserted;
  animation-duration: 1ms;
}

@keyframes button-inserted {
  from { opacity: 0.9999999 }
  to   { opacity: 1 }
}
```

Then you can update the `DOMContentLoaded` event listener as follows:

```jsx
document.addEventListener('DOMContentLoaded', () => {
  replaceWithShareButton(document.querySelectorAll('.share-button'));

  document.addEventListener('animationstart', handleButtonInsertion, false);
  document.addEventListener('MSAnimationStart', handleButtonInsertion, false);
  document.addEventListener('webkitAnimationStart', handleButtonInsertion, false);
});

function handleButtonInsertion (evt) {
  if (evt.animationName === 'button-inserted') {
    replaceWithShareButton([evt.target]);
  }
}
```

With these changes and additions, everything works just like before with the `MutationObserver` API, but with a little more support for older browsers. However, when compared with the `MutationObserver` API, using the CSS animation technique is very limited in the extent of DOM modifications it can be used to observe.

## First, last, and only child

We’ve already seen how we can detect new node insertions using the CSS animation technique, but what more can we achieve with this?

Let’s say we want to ensure that an element is always the first, last, or only child of its parent, irrespective of whether a new node is added into or removed from the parent.

We can use the CSS animation technique like so:

```jsx
.force-first:not(:first-child),
.force-last:not(:last-child),
.force-only:not(:only-child) {
  animation-name: reset-child-position;
  animation-duration: 1ms;
}

@keyframes reset-child-position {
  from { opacity: 0.9999999 }
  to   { opacity: 1 }
}
```

Here, we are using three classes — `.force-first`, `.force-last`, and `.force-only` — to designate the target element to always be the first, last, or only child of its parent, respectively.

Notice the use of the `:not()` pseudo-class together with the `:first-child`, `:last-child`, and `:only-child` pseudo-classes to trigger a CSS animation that will be used to ensure the target element is positioned correctly within its parent after every node insertion.

The event listener we need to ensure these behaviors is as follows:

```jsx
document.addEventListener('DOMContentLoaded', () => {
  document.addEventListener('animationstart', resetChildNodePosition, false);
  document.addEventListener('MSAnimationStart', resetChildNodePosition, false);
  document.addEventListener('webkitAnimationStart', resetChildNodePosition, false);
});

function resetChildNodePosition (evt) {
  if (evt.animationName === 'reset-child-position') {
    const elem = evt.target;
    const parent = elem.parentNode;
    const classes = elem.classList;
    const clonedElem = elem.cloneNode(true);

    const lastChild = parent.lastChild;
    const firstChild = parent.firstChild;

    if (classes.contains('force-only')) {
      if (elem !== firstChild || elem !== lastChild) {
        parent.innerHTML = '';
        parent.appendChild(clonedElem);
      }
      return;
    }

    if (classes.contains('force-last')) {
      if (elem !== parent.lastChild) {
        parent.removeChild(elem);
        parent.appendChild(clonedElem);
      }
      return;
    }

    if (classes.contains('force-first')) {
      if (elem !== parent.firstChild) {
        parent.removeChild(elem);
        parent.insertBefore(clonedElem, parent.firstChild);
      }
      return;
    }
  }
}
```

A careful observation of the above code snippets will indicate that just one element within the parent is expected to be designated as `.force-first`, `.force-last`, or `.force-only` at any time.

So what happens when we have more than one element within the parent element designated as, say, `.force-first`? Well, we run into some problems with our code there.

The following gif shows a simple demonstration of this problem, where two elements inside the same parent are both struggling to always be the first child.

![Two Elements Struggling For The First Child Position](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/elements-struggling-first-child-position.gif?resize=730%2C234&ssl=1)<figcaption id="caption-attachment-13745">Two elements struggling for the first child position.</figcaption>

To fix this issue, we have to decide which element wins in the case of multiple elements struggling for the same position. Here are some reasonable decisions we can make:

- Only the first element within the parent with a `.force-only` or `.force-first` class should be considered for the corresponding child position
- Only the last element within the parent with a `.force-last` class should be considered to always be the last child

Based on these decisions, here are the modifications we have to make:

```jsx
// ...code truncated here...

if (classes.contains('force-only')) {
  const target = parent.querySelectorAll('.force-only')[0];

  if (elem === target && !(elem === firstChild && elem === lastChild)) {
    parent.innerHTML = '';
    parent.appendChild(clonedElem);
  }

  return;
}

if (classes.contains('force-last')) {
  const targets = parent.querySelectorAll('.force-last');
  const target = targets[targets.length - 1];

  if (elem === target && elem !== parent.lastChild) {
    parent.removeChild(elem);
    parent.appendChild(clonedElem);
  }

  return;
}

if (classes.contains('force-first')) {
  const target = parent.querySelectorAll('.force-first')[0];

  if (elem === target && elem !== parent.firstChild) {
    parent.removeChild(elem);
    parent.insertBefore(clonedElem, parent.firstChild);
  }

  return;
}

// ...code truncated here...
```

## Without child

In the previous section, we were majorly concerned about controlling the behavior of some child elements within their parent. It is already very interesting to see how this technique could be used in ensuring several DOM elements maintain specific positions within their parent.

In this section, let’s explore other ways we can put this technique to use by controlling the behavior of an element when it is with or without child(ren).

Let’s say we want to ensure that an element never has a child, i.e., we always want the element to be empty. Again, we can apply the CSS animation technique by creating style rules that look like this:

```jsx
.without-child:not(:empty) {
  animation-name: element-not-empty;
  animation-duration: 1ms;
}

@keyframes element-not-empty {
  from { opacity: 0.9999999 }
  to   { opacity: 1 }
}
```

Also, we will set up event listeners like so:

```jsx
document.addEventListener('DOMContentLoaded', () => {
  document.addEventListener('animationstart', removeNodeChildren, false);
  document.addEventListener('MSAnimationStart', removeNodeChildren, false);
  document.addEventListener('webkitAnimationStart', removeNodeChildren, false);
});

function removeNodeChildren (evt) {
  if (evt.animationName === 'element-not-empty') {
    const elem = evt.target;

    if (elem.children.length > 0) {
      elem.innerHTML = '';
    }
  }
}
```

Well, that was pretty easy, given what we’ve already done before now. That said, we can still explore other ways of handling empty elements.

Let’s say we want to ensure that an element is never empty — this is kinda the reverse of our previous example. If the element happens to be empty, let’s say we want to simply remove it from the DOM. Here are some style rules we can create for that:

```jsx
.no-without-child:empty {
  animation-name: element-empty;
  animation-duration: 1ms;
}

@keyframes element-empty {
  from { opacity: 0.9999999 }
  to   { opacity: 1 }
}
```

The event listeners should be set up as follows:

```jsx
document.addEventListener('DOMContentLoaded', () => {
  document.addEventListener('animationstart', removeEmptyNode, false);
  document.addEventListener('MSAnimationStart', removeEmptyNode, false);
  document.addEventListener('webkitAnimationStart', removeEmptyNode, false);
});

function removeEmptyNode (evt) {
  if (evt.animationName === 'element-empty') {
    const elem = evt.target;
    elem.parentNode.removeChild(elem);
  }
}
```

## Conclusion

In this article, we’ve been able to see how CSS animations can be leveraged for observing DOM mutations — particularly node insertions and node emptiness. While this technique can be explored further in areas of observing element attributes, it is generally impractical for that purpose.

It is clearly not as powerful as the `MutationObserver` API in so many ways, and should only be considered as an alternative if supporting old browsers is of particular concern, and the mutations to be observed are within the scope of what was discussed in this article.

That said, I am building a very simple and lightweight JavaScript library for the purpose of observing and reacting to DOM mutations based on the CSS animations technique discussed in this article. I will be [tweeting a lot about it](https://twitter.com/gladchinda) — watch out for it.

I’m glad you made it to the end of this article. It was quite a lengthy one, and I do hope it was worth your while. As always, please remember to:

- **Comment** your feedback
- **Share** with someone
- [**Follow**](https://twitter.com/gladchinda) me on Twitter

HAPPY CODING!!!

* * *
## Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, [try LogRocket.](https://logrocket.com/signup/) 

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/403rye2ptzx994uhktk1.png)

[LogRocket](https://logrocket.com/signup/) is like a DVR for web apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web apps — [Start monitoring for free.](https://logrocket.com/signup/)
* * *

The post [CSS animations for DOM observation](https://blog.logrocket.com/css-animations-for-dom-observation/) appeared first on [LogRocket Blog](https://blog.logrocket.com).