skip to Main Content

I’m trying to create a website in which there are some simple CSS animations:

  1. fade-in: Causes the element to fade into view, and
  2. error: 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?

2

Answers


  1. You can try overriding the class when removing it by setting the opacity attribute from JavaScript:

    elem.style.opacity = 0;
    

    Another thing to try is by adding "!important" to force "animation: none" to be applied, or some other way to highen the specificity.

    .no_transition {
        animation: none !important;
        transition: none !important;
    }
    

    You can also override the opacity in .no_transition:

    .no_transition {
        animation: none !important;
        transition: none !important;
        opacity: 0 !important;
    }
    

    But you should of course try using CSS specificity over !important.

    Login or Signup to reply.
  2. 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>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search