I'm trying to create a website in which there are some simple CSS animations:
fade-in
: Causes the element to fade into view, anderror
: Causes the element to turn red and shake for a brief moment.When I trigger the error
animation through JS, once it's done, the fade-in
animation plays as well, which is not desired. Here's the CSS and JS:
style.css:
...
/* All children of the `splash` class are subject to the `fade-in` animation */
.splash * {
animation: fade-in 1s ease-in;
opacity: 1;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.error {
animation: shake 0.4s ease-in-out;
border-color: red;
}
@keyframes shake {
0% { transform: translateX(0); }
25% { transform: translateX(-5px); }
50% { transform: translateX(5px); }
75% { transform: translateX(-5px); }
100% { transform: translateX(0px); }
}
.no_transition {
animation: none;
transition: none;
}
...
script.js:
...
function err(elem, msg) {
if (msg) alert(msg);
// start the transition
elem.classList.add('error');
elem.disabled = true;
setTimeout(() => {
// 1 second later, remove the `error` class.
elem.classList.add('no_transition');
elem.classList.remove('error');
elem.disabled = false;
elem.classList.remove('no_transition'); // this is what triggers the `fade-in` animation again
}, 1000);
}
...
Since adding / removing a class will trigger the CSS animation, I tried creating a separate class (no-transition
) that blocks all animations and transitions. However, once I remove it, fade-in
triggers once again.
Is there a way to work around this, and add / remove these CSS classes without triggering the fade-in
animation?
You could set your animation
so that it contains the two sets of animations, this way the first one wouldn't restart:
document.querySelector(".splash").addEventListener("click", ({
target: elem,
currentTarget
}) => {
if (elem === currentTarget) {
return;
}
elem.classList.add('error');
setTimeout(() => {
// 1 second later, remove the `error` class.
elem.classList.remove('error');
}, 1000);
})
console.log("click on either element to trigger the .error class");
.splash * {
border: 1px solid;
animation: fade-in 1s ease-in;
opacity: 1;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.error {
/* Also set the fade-in class */
animation: fade-in 1s ease-in, shake 0.4s ease-in-out;
border-color: red;
}
@keyframes shake {
0% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
50% {
transform: translateX(5px);
}
75% {
transform: translateX(-5px);
}
100% {
transform: translateX(0px);
}
}
<div class="splash">
<div>
foo
</div>
<div>
bar
</div>
</div>
But that implies the fade-in
animation is always active when the shake
one is.
So it actually sounds like you don't want fade-in
to be an animation, but a transition instead. If you have trouble making it kick when appending your elements, then see this Q/A. Basically, you have to wait for (or force) a reflow before you set the target of the animation.
// Wait for next browser reflow
const observer = new ResizeObserver(() => {
observer.disconnect();
document.querySelector(".splash").classList.add("in");
});
observer.observe(document.body);
document.querySelector(".splash").addEventListener("click", ({
target: elem,
currentTarget
}) => {
if (elem === currentTarget) {
return;
}
elem.classList.add('error');
setTimeout(() => {
// 1 second later, remove the `error` class.
elem.classList.remove('error');
}, 1000);
})
console.log("click on either element to trigger the .error class");
.splash * {
border: 1px solid;
transition: opacity 1s ease-in;
opacity: 0;
}
.splash.in * {
opacity: 1;
}
.error {
animation: shake 0.4s ease-in-out;
border-color: red;
}
@keyframes shake {
0% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
50% {
transform: translateX(5px);
}
75% {
transform: translateX(-5px);
}
100% {
transform: translateX(0px);
}
}
<div class="splash">
<div>
foo
</div>
<div>
bar
</div>
</div>
But, since you're gonna use JS to control these transitions and animations, then it's even better to use the Web Animation API to control these directly rather than do a complex round-trip through DOM then CSSOM.
// fade-in all children at page load
document.querySelectorAll(".splash *").forEach((elem) => {
elem.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 });
});
// shake on click
document.querySelector(".splash").addEventListener("click", ({target: elem, currentTarget}) => {
if (elem === currentTarget) { return; }
elem.animate([
{ transform: "translateX(0)" },
{ transform: "translateX(-5px)" },
{ transform: "translateX(5px)" },
{ transform: "translateX(-5px)" },
{ transform: "translateX(0px)" },
], { duration: 400 });
// We can even use it for non animated temporary styles like the 1s border-color
elem.animate([{ borderColor: "red" }, { borderColor: "red" }], { duration: 1000 })
// .finished.then(() => { the animation is over });
// If you want to do something after the animation is over
})
console.log("click on either element to trigger the .error class");
.splash * {
border: 1px solid;
}
<div class="splash">
<div>
foo
</div>
<div>
bar
</div>
</div>