2

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;
});```

4 Answers 4

5

Define the animation as a let, pause it on click and re-define its behavior depending on the boolean value:

// Getting everything
const button = document.querySelector('.button');
const box = document.querySelector('.box');

// Keyframes
const keyframes = [{
    border: '2px solid red',
    width: '100px',
    backgroundColor: 'blue'
  },
  {
    border: '6px solid green',
    width: '150px',
    backgroundColor: 'purple'
  },
];

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

let reverse = false;
let anim;

button.addEventListener('click', () => {
  if (anim) anim.pause();
  anim = box.animate(keyframes, options)[reverse ? "reverse" : "play"]();
  reverse = !reverse;
});
* {
  box-sizing: border-box;
  margin: 0;
}

body {overflow: hidden;}

.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;
  user-select: none;
}

.button:hover {
  scale: 1.05;
  box-shadow: 0 0 3px 1px #00000060;
}

.button:active {
  scale: 0.9;
}

/* Code that matters  */
.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, a better variant for that specific case would be using Element.classList.toggle() and transition instead of animation.
This way there's an even better interpolation (no hard-resets) handled automagically by the browser.

const button = document.querySelector('.button');
const box = document.querySelector('.box');

button.addEventListener('click', () => {
  box.classList.toggle("is-active");
});
* {
  box-sizing: border-box;
  margin: 0;
}

body { overflow: hidden; }

.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:active {
  scale: 0.9;
}

/* Code that matters  */
.box {
  background-color: blue;
  border: 2px solid red;
  aspect-ratio: 1;
  width: 100px;
  transition: width 0.2s ease-in, border 0.2s ease-in;

  &.is-active {
    border: 6px solid green;
    width: 150px;
    background-color: purple;
  }
}
<div class="container">
  <div class="box"></div>
  <button class="button">Click Me</button>
</div>

Sign up to request clarification or add additional context in comments.

2 Comments

The better way could definitely be the one that includes CSS Transitions, but I was wondering if we could achieve this exact same with the WAAPI...... Even in your implementation if we double click, the animation gets reversed from the 100% and not the seek point where it was when the button is pressed.
@MuhammadAbdullah sadly (AFAIK) the best you can do is get the current state using DOMMatrix or const progress = anim?.effect.getComputedTiming().progress ?? 0; and (linearly) interpolate the new value (depending on the animation direction). Meaning that you'll have to get the [0.0-1.0] progress value and pass a new keyframe calculation effect. For width it's quite trivial, but for other properties like color (specially if you use HEX instead of i.e: HSL) would become a parsing mess.
1

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 :)

Comments

-1

The issue you're facing is due to how the Web Animations API handles animation reversal and state management. Try the below JavaScript

const button = document.querySelector('.button');
const box = document.querySelector('.box');
    
// Keyframes
const keyframes = [
  { border: '2px solid red', width: '200px', backgroundColor: 'blue' },
  { border: '6px solid green', width: '250px', backgroundColor: 'purple' }
];
    
// Options
const options = {
  duration: 200,
  easing: 'ease-in',
  fill: 'both'
};
    
let animation = box.animate(keyframes, {
  ...options,
  direction: 'normal',
  autoplay: false
});
    
button.addEventListener('click', () => {
  // Cancel the current animation to prevent conflicts
  animation.cancel();
      
  // Create a new animation with the appropriate direction
  animation = box.animate(keyframes, {
  ...options,
  direction: animation.effect.getComputedTiming().direction === 'normal' ? 'reverse' : 'normal'
  });
      
  // Play the new animation
  animation.play();
});

3 Comments

Where is autoplay: false (in Animation API) documented?
To prevent the animation from starting automatically. The animation should play when the button is clicked.
That's not what happens (try to run your own code) - That's why I'm asking, where is that autoplay property documented in the Animation API for the Keyframe Options object? Seems like a property most likely invented by some AI :)
-3

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <style>
    #box {
      width: 100px;
      height: 100px;
      background: blue;
    }
  </style>
</head>

<body>
  <div id="box"></div>
  <button id="toggleBtn">Toggle</button>

  <script>
    const box = document.getElementById('box');
    const toggleBtn = document.getElementById('toggleBtn');

    const keyframes = [{
        transform: 'scale(1)',
        background: 'blue'
      },
      {
        transform: 'scale(1.5)',
        background: 'green'
      }
    ];

    const options = {
      duration: 300,
      fill: 'both' // keeps final styles
    };

    const animation = box.animate(keyframes, options);
    animation.pause(); // start paused
    let direction = 'forwards';

    toggleBtn.addEventListener('click', () => {
      // Handle reversing from current point
      animation.reverse();
      if (animation.playState !== 'running') {
        animation.play();
      }
    });
  </script>
</body>

</html>

the above code could easily work on any browser engine see most of the browsers like chrome safari and edge support web animation apis , so your animation should work fine make sure that you have the latest version of the engines if you pc is old (especially older versions of Internet Explorer or some outdated mobile browsers), the animation might not work properly or show up at all. It's a good idea to check which browsers your audience uses.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.