DEV Community

Cover image for Collapsing Page Effect
Johnny Fekete
Johnny Fekete

Posted on • Edited on

Collapsing Page Effect

Fun with the Logout Animation

The other day I was working on my startup, Bonboarding, 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,
it got viral, especially after it was retweeted by Smashing Magazine.

Collapsing page effect animation

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 - 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.

const elements = document.querySelectorAll('body > *, body div');
Enter fullscreen mode Exit fullscreen mode

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:

document.body.style.overflow = 'hidden';
document.body.style.pointerEvents = 'none';
Enter fullscreen mode Exit fullscreen mode

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):

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

const height = Math.max(
  body.scrollHeight,
  body.offsetHeight,
  html.clientHeight,
  html.scrollHeight,
  html.offsetHeight,
);
Enter fullscreen mode Exit fullscreen mode

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:

[...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)`;
});
Enter fullscreen mode Exit fullscreen mode

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:

// 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
});
Enter fullscreen mode Exit fullscreen mode

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

Here's the final code:

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)`;
  });
}
Enter fullscreen mode Exit fullscreen mode

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.


You can grab the whole code as an NPM package - it can be used both as a React component or as a standalone JavaScript function.

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).

Top comments (5)

Collapse
 
ekeijl profile image
Edwin • Edited

Looks really cool! But the demo link on NPM does not seem to work? You should add a Codesandbox example in this article.

Also, I think you can use DOMElement.contains() instead of the isDescendant function?

Collapse
 
johnnyfekete profile image
Johnny Fekete

I tried to find which is the link that doesn't work. Is it the one saying try it yourself at the npm page?

If so, you need to click on the "Logout" button to see the animation

Collapse
 
johnnyfekete profile image
Johnny Fekete

Thanks Edwin,
Good points, I didn't know about the contains() method but seems like it could work, also has a great browser support!

Collapse
 
johnnyfekete profile image
Johnny Fekete

Thanks a lot, Harsh!