javascriptcsscss-animationscss-transitionsweb-animations-api

Need help in toggeling animation with CSS Web Animations API


I am trying to toggle an animation on an element using the CSS Web Animations API (WAAPI). Specifically, I want the animation to transition smoothly between two states on each button click — essentially playing forward on the first click, then reversing on the second, and so on.

I’ve set up my keyframes and options correctly, and the animation runs fine the first time, but from the second toggle onward, the animation becomes jittery and visually “janky”.

!Important! I also want the box to get the end of animation styles.

I want the animation to respond correctly even when the user clicks the button rapidly, ensuring that it reverses immediately from its current state without any delay or visual glitches.

Here is a link to code pen where I was replicated the issue: https://codepen.io/mab141211/pen/VYYoOjw

const keyframes = [
  { border: '2px solid red', width: '200px', backgroundColor: 'blue', offset: 0, },
  { border: '6px solid green', width: '250px', backgroundColor: 'purple', offset: 1, },
];

// Options
const options = {
  duration: 200,
  easing: 'ease-in',
  fill: 'both',
};

let isPlayingForward = true;

const animation = box.animate(keyframes, options);
animation.pause();

button.addEventListener('click', () => {
  if (isPlayingForward) {
    animation.play();
  } else {
    animation.reverse();
  }
  
  isPlayingForward = !isPlayingForward;
});```

Solution

  • The answer from @RokoC.Buljan about WAAPI Works great for animation where we don't care about reversing from in between and then the comment specifies taking the progress of the animation and then interpolating the new value. Thanks for the motivation.

    After testing different stuff I came up with this solution that makes the animation similar to the CSS Transitions' animation. Below is the Code with explanation in comments:

    // Getting everything
    const button = document.querySelector('.button');
    const box = document.querySelector('.box');
    
    // Keyframes
    const keyframes = [{
        border: '2px solid red',
        width: '100px',
        backgroundColor: 'blue',
        offset: 0
      },
      {
        border: '6px solid green',
        width: '150px',
        backgroundColor: 'purple',
        offset: 1
      },
    ];
    
    // Options
    const options = {
      duration: 300,
      easing: 'ease-in',
      fill: 'both',
    };
    
    // Creating animation and instantly pausing it.
    const anim = box.animate(keyframes, options);
    anim.pause();
    
    // The play state automaticaly changes from finished to
    // running even if only the sign of playbackRate is changed.
    setInterval(() => {
      console.log(anim.playState);
    }, 100)
    
    
    document.querySelector('.button').addEventListener('click', () => {
      // Playing the animation for the first click and then
      // After first iteration only setting the Animation.playbackRate *= -1
      // or saying Animation.reverse() would make the animation change direction
      // mid animation and also after the animation is finished,
      // it would reverse the animation direction and put it in a running state.
      // Basically "reverse" reverses the animation, no matter where the animation is.
      if (anim.playState === 'paused') {
        anim.play();
      } else {
        anim.playbackRate *= -1;
        // same as:
        // anim.reverse();
      }
    });
    * {
      box-sizing: border-box;
      margin: 0;
    }
    
    body {
      overflow: clip;
    }
    
    .container {
      background-color: black;
      display: flex;
      gap: 1.5rem;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }
    
    .button {
      background-color: #33b864;
      border: none;
      padding-inline: 2rem;
      padding-block: 1rem;
      scale: 1;
      box-shadow: 0 0 0 0 #00000060;
      transition: scale 100ms ease-in, box-shadow 100ms ease-in;
    }
    
    .button:hover {
      scale: 1.05;
      box-shadow: 0 0 3px 1px #00000060;
    }
    
    .button:touch {
      scale: 2;
    }
    
    .button:active {
      scale: 0.9;
    }
    
    .box {
      background-color: blue;
      border: 2px solid red;
      aspect-ratio: 1;
      width: 100px;
    }
    <div class="container">
      <div class="box"></div>
      <button class="button">Click Me</button>
    </div>

    PS @RokoC.Buljan Please let me know if my question was vague and/or if I can further improve the code above. Thanks :)