Search code examples
javascriptecmascript-6css-animationses6-promise

How to chain CSS animations using Promise?


I'm trying to figure out how Promise works and to make one CSS animation to run after another. But they run simultaneously...

JS fiddle

let move_boxes = () => {
    return new Promise( (resolve, reject) => {
        resolve ('boxes moved!')
    })
};
let move_box_one = () => {
    return new Promise( (resolve, reject) => {
        resolve (document.getElementById('div_two').style.animation = 'move 3s forwards')
        console.log('box one moved!')   
    })

}
let move_box_two = () => {
    return new Promise( (resolve, reject) => {
        resolve (document.getElementById('div_one').style.animation = 'move 3s forwards')
        console.log('box two moved!')       
    })
}

move_boxes().then(() => {
    return move_box_one();
}).then(() => {
    return move_box_two();
});

Solution

  • In contrary to the answers posted, I would discourage using window.setTimeout, because it does not guarantee that the timers are in sync with the animation end events, which sometimes are offloaded to the GPU. If you want to be extra sure, you should be listening to the animationend event, and resolving it in the callback itself, i.e.:

    let move_box_one = () => {
      return new Promise((resolve, reject) => {
        const el = document.getElementById('div_one');
        const onAnimationEndCb = () => {
          el.removeEventListener('animationend', onAnimationEndCb);
          resolve();
        }
    
        el.addEventListener('animationend', onAnimationEndCb)
        el.style.animation = 'move 3s forwards';
      });
    }
    

    Even better, is that since you're writing somewhat duplicated logic for both boxes, you can abstract all that into a generic function that returns a promise:

    // We can declare a generic helper method for one-time animationend listening
    let onceAnimationEnd = (el, animation) => {
      return new Promise(resolve => {
        const onAnimationEndCb = () => {
          el.removeEventListener('animationend', onAnimationEndCb);
          resolve();
        }
        el.addEventListener('animationend', onAnimationEndCb)
        el.style.animation = animation;
      });
    }
    
    let move_box_one = async () => {
      const el = document.getElementById('div_one');
      await onceAnimationEnd(el, 'move 3s forwards');
    }
    let move_box_two = async () => {
      const el = document.getElementById('div_two');
      await onceAnimationEnd(el, 'move 3s forwards');
    }
    

    Also, your move_boxes function is a little convoluted. If you want to run the individual box-moving animations asynchronous, declare it as an async method and simply await individual box moving function calls, i.e.:

    let move_boxes = async () => {
      await move_box_one();
      await move_box_two();
    }
    move_boxes().then(() => console.log('boxes moved'));
    

    See proof-of-concept example (or you can see it from this JSFiddle that I've forked from your original one):

    // We can declare a generic helper method for one-time animationend listening
    let onceAnimationEnd = (el, animation) => {
      return new Promise(resolve => {
        const onAnimationEndCb = () => {
          el.removeEventListener('animationend', onAnimationEndCb);
          resolve();
        }
        el.addEventListener('animationend', onAnimationEndCb)
        el.style.animation = animation;
      });
    }
    
    let move_box_one = async () => {
      const el = document.getElementById('div_one');
      await onceAnimationEnd(el, 'move 3s forwards');
    }
    let move_box_two = async () => {
      const el = document.getElementById('div_two');
      await onceAnimationEnd(el, 'move 3s forwards');
    }
    
    let move_boxes = async () => {
      await move_box_one();
      await move_box_two();
    }
    move_boxes().then(() => console.log('boxes moved'));
    #div_one {
      width: 50px;
      height: 50px;
      border: 10px solid red;
    }
    
    #div_two {
      width: 50px;
      height: 50px;
      border: 10px solid blue;
    }
    
    @keyframes move {
      100% {
        transform: translateX(300px);
      }
    }
    
    @keyframes down {
      100% {
        transform: translateY(300px);
      }
    }
    <div id="div_one"></div>
    <div id="div_two"></div>