skip to Main Content

I have the following example code which creates toasts that fly out from the top of the screen, stay for a few seconds, and then fly away:

function createToast() {
  const elem = document.createElement("div");
  elem.classList.add("toast");
  elem.innerText = "hello";

  document.body.appendChild(elem);

  setTimeout(() => elem.remove(), 3000);
}

setInterval(createToast, 3000);
body {
  background-color: #d7d7d7;
}

.toast {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 5px 10px;
  max-width: 200px;
  transform: translate(0, 0);
  background-color: #fff;
  border-radius: 50px;

  animation-name: toastin, toastout;
  animation-duration: 0.3s, 0.5s;
  animation-iteration-count: 1, 1;
  animation-delay: 0s, 2.5s;
  animation-fill-mode: forwards, forwards;
}

@keyframes toastin {
  from {
    opacity: 0;
    transform: translate(0, -40px);
  }
  to {
    opacity: 1;
    transform: translate(0, 20px);
  }
}

@keyframes toastout {
  from {
    opacity: 1;
    transform: translate(0, 20px);
  }
  to {
    opacity: 0;
    transform: translate(50vw, 20px);
  }
}
<body><body>

This works fine, but sometimes in my application another toast will be created before the current one is gone. In that case, I want the current toast to fly away immediately upon the creation of the new toast, instead of waiting 2.5 seconds.

For the life of me, I cannot figure this out. Is it possible? It seems so simple, but nothing I try seems to have any effect.

First I tried grabbing the existing toast element and setting the animation-delay to 0s, but that had no effect. Then I tried replacing it with an entire new animation, tried using !important, etc. In all cases the toast just, like, disappears, with no animation happening.

Is there any way to just make the toast immediately finish its current animation in like 0.4 seconds when a new toast is created? So that it flies away before it collides with the incoming toast?

4

Answers


  1. You need to keep a reference to elem and remove it when the function is called again. Maybe something like this:

    let activeToast;
    
    function clearToast() {
      if(!activeToast){
         return;
      }
      activeToast.remove();
      activeToast = null;
    }
    
    function createToast() {
      clearToast();
      const elem = document.createElement("div");
      elem.classList.add("toast");
      elem.innerText = "hello";
      activeToast = elem;
      document.body.appendChild(elem);
    
      setTimeout(() => clearToast(), 3000);
    }
    
    setInterval(createToast, 3000);
    

    But if you want to keep track of the animation itself there are some animation events you might want to use.

    https://medium.com/front-end-weekly/css-animation-events-in-javascript-d2bfe702636d

    Login or Signup to reply.
  2. You’d think it’s possible, right? Maybe you need to add a variable to check whether there’s a toast being displayed or not. If there’s a toast being displayed, you can apply elem.remove() right away to only show the new toast. Check below to see if that works for you.

    let isToastDisplayed = false;
    
    function createToast() {
      const elem = document.createElement("div");
      elem.classList.add("toast");
      elem.innerText = "hello";
    
      document.body.appendChild(elem);
    
      if (isToastDisplayed) {
        const currentToast = document.querySelector(".toast");
        currentToast.remove();
      }
    
      isToastDisplayed = true;
    
      setTimeout(() => {
        elem.remove();
        isToastDisplayed = false;
      }, 3000);
    }
    
    setInterval(createToast, 3000);
    body {
      background-color: #d7d7d7;
    }
    
    .toast {
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 5px 10px;
      max-width: 200px;
      transform: translate(0, 0);
      background-color: #fff;
      border-radius: 50px;
    
      animation-name: toastin, toastout;
      animation-duration: 0.3s, 0.5s;
      animation-iteration-count: 1, 1;
      animation-delay: 0s, 2.5s;
      animation-fill-mode: forwards, forwards;
    }
    
    @keyframes toastin {
      from {
        opacity: 0;
        transform: translate(0, -40px);
      }
      to {
        opacity: 1;
        transform: translate(0, 20px);
      }
    }
    
    @keyframes toastout {
      from {
        opacity: 1;
        transform: translate(0, 20px);
      }
      to {
        opacity: 0;
        transform: translate(50vw, 20px);
      }
    }
    <body><body>
    Login or Signup to reply.
  3. Not sure if this directly answer your question but you might be able to separate the animation out on a separate class… then with js you can check for existing ones and add the class out!

    check it out

    function createToast() {
    const existing = document.querySelectorAll('.toast')
    if(existing.length > 0) {
    existing.forEach(e => {
    e.classList.add('toast-out')
    })
    }
      const elem = document.createElement("div");
      elem.classList.add("toast");
      elem.innerText = "hello";
    
      document.body.appendChild(elem);
    
      setTimeout(() => elem.remove(), 3000);
    }
    
    setInterval(createToast, 2000);
    body {
      background-color: #d7d7d7;
    }
    
    .toast {
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 5px 10px;
      max-width: 200px;
      transform: translate(0, 0);
      background-color: #fff;
      border-radius: 50px;
    
      animation-name: toastin;
      animation-duration: 0.3s;
      animation-iteration-count: 1;
      animation-delay: 0s;
      animation-fill-mode: forwards;
    }
    
    .toast-out {
    animation-name: toastout;
      animation-duration: 0.5s;
      animation-iteration-count: 1;
      animation-delay: 0s;
      animation-fill-mode: forwards;
    }
    
    @keyframes toastin {
      from {
        opacity: 0;
        transform: translate(0, -40px);
      }
      to {
        opacity: 1;
        transform: translate(0, 20px);
      }
    }
    
    @keyframes toastout {
      from {
        opacity: 1;
        transform: translate(0, 20px);
      }
      to {
        opacity: 0;
        transform: translate(50vw, 20px);
      }
    }
    <body><body>
    Login or Signup to reply.
  4. I made some adjustments to your approach:

    1. Instead of assigning both of the animations at the same time, only assign the toastout animation to a certain class, let’s say .out.

    2. When trying to remove a toast, call function removeToast(). It adds class .out to that toast. So you can control when to play the toastout animation, then after the animation is played, remove the element using event animationend.

    3. Each time a new toast is added, try to call removeToast() on every existing toast.

    Here’s the full solution:

    function createToast() {
      // remove all existing toasts
      [...document.querySelectorAll('.toast')].forEach(removeToast);
      
      const elem = document.createElement("div");
      elem.classList.add("toast");
      elem.innerText = "hello";
      setTimeout(() => removeToast(elem), 3000);
      document.body.appendChild(elem);
    }
    
    function removeToast(toast) {
      // if a toast is already removed, ignore
      if (!toast || toast.classList.contains('out')) return;
      
      toast.classList.add('out');
      toast.addEventListener('animationend', () => toast.remove());
    }
    body {
      background-color: #d7d7d7;
    }
    
    .toast {
      display: flex;
      position: fixed;
      justify-content: center;
      align-items: center;
      padding: 5px 10px;
      max-width: 200px;
      transform: translate(0, 0);
      background-color: #fff;
      border-radius: 50px;
    
      animation-name: toastin;
      animation-duration: 0.3s;
      animation-iteration-count: 1;
      animation-fill-mode: forwards;
    }
    
    .toast.out {
      animation-name: toastout;
    }
    
    @keyframes toastin {
      from {
        opacity: 0;
        transform: translate(0, -40px);
      }
      to {
        opacity: 1;
        transform: translate(0, 20px);
      }
    }
    
    @keyframes toastout {
      from {
        opacity: 1;
        transform: translate(0, 20px);
      }
      to {
        opacity: 0;
        transform: translate(50vw, 20px);
      }
    }
    <body>
      <button onclick="createToast()">Add toast</button>
    <body>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search