Collapsing Page Effect
Tags: javascript, animation, ux, react
Published: 2020-11-28 14:18:13


## Fun with the Logout Animation

The other day I was working on [my startup, Bonboarding](https://bonboarding.com), and wanted to spice things up
a bit so I created a collapsing page animation for the logout functionality.
Nothing fancy, some CSS transition animation. But when I [posted it on Twitter](https://twitter.com/FullStackMaker/status/1280413902610796544?s=20),
it got viral, especially after it was retweeted by Smashing Magazine.

![Collapsing page effect animation](https://www.bonboarding.com/gifs/collapsing-page-effect/collapse.gif)

I was totally mind-blown by the engagement, and all the positive feedback
(this was my first viral content). Many of the people asked me to share the code,
but instead of just publishing it on github (which I did, and you can access it as
a [NPM package here](https://www.npmjs.com/package/collapsing-page) - available both for React or plain JavaScript) I decided to write a brief article about it.

## The Not-So-Complicated Code

As a start, I wanted body's _all_ child elements to collapse, and also all `div`'s.
I didn't want to put animation on all elements (eg. headers, links, buttons etc)
because I felt it would make the animation too fractured.

```js
const elements = document.querySelectorAll('body > *, body div');
```

To make sure that the page doesn't get scrolled, I set the position to `fixed`.
I also disabled pointer events, so no clicks or other events get triggered
during the animation:

```js
document.body.style.overflow = 'hidden';
document.body.style.pointerEvents = 'none';
```

Finally, before dealing with the actual, I had to measure the total height
of the page (to know, how much should the items "fall" to ensure that all items
will be out of the screen at the end):

```js
const body = document.body;
const html = document.documentElement;

const height = Math.max(
  body.scrollHeight,
  body.offsetHeight,
  html.clientHeight,
  html.scrollHeight,
  html.offsetHeight,
);
```

So the animation is actually super simple: just loop through the selected
elements and generate some semi-random values, then add them as CSS attributes:

```js
[...elements].forEach(element => {
  const delay = Math.random() * 3000); // wait between 0 and 3 seconds
  const speed = Math.random() * 3000 + 2000; // speed between 2 and 5 seconds
  const rotate = Math.random() * 30 - 15; // rotate with max 15 degrees to either direction
  const moveX = Math.random() * 160 - 80; // move with 80px to either direction

  element.style.transition = `transform ${speed}ms ease-out`;
  element.style.transitionDelay = `${delay}ms`;
  element.style.transform = `translateY(${height * 1.5}px) translateX(${moveX}px) rotate(${rotate}deg)`;
});
```

This loop just goes through every element and assigns random values for them.

All of the elements will be transitioned downward with the height of the screen,
therefore even the ones at the top of your page will end up out of the screen at the end.

Finally, I wanted to keep one item that stayed on the screen behind the collapsing page:

There are a few important things with it:

- it should be a child of the body, so it's parent element is not collapsing
- it should have `fixed` position
- to achieve the effect that it's in the background behind everything else, you can adjust the `z-index`

And then just ignore it and it's children elements in the forEach loop:

```js
// Identify the logout screen that should stay in place
const logoutEl = document.querySelector('#logout-screen');

// Function that tells if an element is a
// descendant (children, grandchildren etc) of another element
const isDescendant = (parent, child) => {
  let node = child.parentNode;
  while (node !== null) {
    if (node === parent) {
      return true;
    }
    node = node.parentNode;
  }
  return false;
};

// And the updated forEach loop:
[...elements].forEach(element => {
  if (element === logoutEl || isDescendant(logoutEl, element)) {
    element.style.pointerEvents = 'all'; // this element should detect clicks
    return; // don't continue adding the animation
  }

  // ... add the animation for the other items
});
```

This is the basic logic, it's quite simple and all animations are handled by CSS transitions.

Here's the final code:

```js
function collapsePage() {
  const elements = document.querySelectorAll('body > *, body div');
  const logoutEl = document.querySelector('#logout-screen');

  const body = document.body;
  const html = document.documentElement;

  const height = Math.max(
    body.scrollHeight,
    body.offsetHeight,
    html.clientHeight,
    html.scrollHeight,
    html.offsetHeight,
  );

  document.body.style.overflow = 'hidden';
  document.body.style.pointerEvents = 'none';

  const isDescendant = (parent, child) => {
    let node = child.parentNode;
    while (node !== null) {
      if (node === parent) {
        return true;
      }
      node = node.parentNode;
    }
    return false;
  };

  [...elements].forEach(element => {
    if (element === logoutEl || isDescendant(logoutEl, element)) {
      element.style.pointerEvents = 'all';
      return;
    }

    element.style.pointerEvents = 'none';

    const delay = Math.random() * 3000; // wait between 0 and 3 seconds
    const speed = Math.random() * 3000 + 2000; // speed between 2 and 5 seconds
    const rotate = Math.random() * 30 - 15; // rotate with max 10 degrees
    const moveX = Math.random() * 160 - 80; // move with 50px to either direction

    element.style.transition = `transform ${speed}ms ease-out`;
    element.style.transitionDelay = `${delay}ms`;
    element.style.transform = `translateY(${height *
      1.5}px) translateX(${moveX}px) rotate(${rotate}deg)`;
  });
}
```

## Things to Consider

After the animation is done, all your elements will still be available in the DOM,
just transitioned out of the screen. It is not a problem if you will navigate to
another page after, but it might cause unexpected behavior if you use some
libraries that handle the navigation for you (eg. react-router-dom).

To solve this issue, I added a reset function to the component, that is triggered
on unmounting.

<strong>
You can grab the whole code as an [NPM package](https://www.npmjs.com/package/collapsing-page) - it can be used both as a React component or as a standalone JavaScript function.
</strong>

While this animation can bring some unexpected delight to your users, be careful with it.
Don't overuse, as the animation takes a few seconds each time. I recommend only using it for logouts,
or when the user deletes something in your web-app (eg. a large project, or even the user's profile).