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.
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');
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';
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,
);
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)`;
});
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
});
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)`;
});
}
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)
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?
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
Thanks Edwin,
Good points, I didn't know about the
contains()
method but seems like it could work, also has a great browser support!Thanks a lot, Harsh!