Restart animation in CSS3: any better way than removing the element?
Asked Answered
V

15

118

I have a CSS3 animation that needs to be restarted on a click. It's a bar showing how much time is left. I'm using the scaleY(0) transform to create the effect.

Now I need to restart the animation by restoring the bar to scaleY(1) and let it go to scaleY(0) again. My first attempt to set scaleY(1) failed because it takes the same 15 seconds to bring it back to full length. Even if I change the duration to 0.1 second, I would need to delay or chain the assignment of scaleY(0) to let the bar replenishment complete. It feels too complicated for such a simple task.

I also found an interesting tip to restart the animation by removing the element from the document, and then re-inserting a clone of it: http://css-tricks.com/restart-css-animation/

It works, but is there a better way to restart a CSS animation? I'm using Prototype and Move.js, but I'm not restricted to them.

Vexation answered 7/6, 2011 at 16:28 Comment(4)
possible duplicate of How do I re-trigger a WebKit CSS animation via JavaScript?Mendicity
You can read in the updated blog post an other technique forcing to reflow the element: element.offsetWidth = element.offsetWidth;Staceystaci
I found cloning was the best solution, as per your CSS-Tricks link.Palladino
TL;DR: e.style.animation = 'none'; e.offsetHeight; e.style.animation = ...; Or, if you're using classes: e.classList.remove('a'); e.offsetHeight; e.classList.add('a');Vareck
L
97

Just set the animation property via JavaScript to "none" and then set a timeout that changes the property to "", so it inherits from the CSS again.

Demo for Webkit here: http://jsfiddle.net/leaverou/xK6sa/ However, keep in mind that in real world usage, you should also include -moz- (at least).

Liegnitz answered 10/6, 2011 at 7:34 Comment(7)
Thanks Lea. Almost there :), If I change your animation to run only once I don't quite get the same effect. When I click, the animation doesn't start over again.Vexation
Thanks a lot! - But unfortunately i cannot get it to work in Safari. Chrome, Edge and Firefox are working as expected. i use following code: var anim = jQuery(mutation.target).find(".background.background-image:first").get(0); anim.style.WebkitAnimation = 'none'; anim.style.animation = 'none'; setTimeout(function() { anim.style.WebkitAnimation = ''; anim.style.animation = ''; }, 10); }Perception
Not good enough, because sometimes a flaw could be seen if timeout is too long, and the effect isn't taken if timeout is too short. Not recommended if you need to restart animation when it is still playing.Levator
It's 2019 now, vendor prefix for this should no longer be necessary.Cookhouse
To avoid the timeout problems described by @Eric, you can call void element.offsetWidth; to force a reflow in between the animation property changes instead of using a timeout.Ilex
How to achieve it for multiple buttons ? how to start animation for multiple buttons ?Lighten
As of 2024, this should no longer be the accepted answer. The Element.getAnimations() or Document.getAnimations() methods are much cleaner. V. Rubinetti's answer should now be the accepted answer.Hebron
S
149

No need in timeout, use reflow to apply the change:

function reset_animation() {
  var el = document.getElementById('animated');
  el.style.animation = 'none';
  el.offsetHeight; /* trigger reflow */
  el.style.animation = null; 
}
#animated {
  position: absolute;
  top: 70px;
  width: 50px; height: 50px;
  background-color: black;
  animation: bounce 3s ease-in-out infinite;
}
@keyframes bounce {
  0% { left: 0; }
  50% { left: calc( 100% - 50px ); }
  100% { left: 0; }
}
<div id="animated"></div>
<button onclick="reset_animation()">Reset</button>
Stinkpot answered 11/7, 2017 at 14:2 Comment(7)
You can also trigger reflow by calling any of these properties/method, not just offsetHeight.Particularity
doesn't triggering a reflow have a performance hit? how does this compare to the selected answer, performance-wise?Sulphonate
This doesn't work correctly on an element with more than 1 animation applied to it.Adeleadelheid
@card100, could you provide an example?Stinkpot
What is the line el.style.animation = null; doing in this code?Turku
For Typescript users, you'll want to use el.style.animation = ''; instead of the above, as animation is expecting a string.Gamaliel
@Turku The way this works is by overriding your CSS with a style attribute, which takes precedence. Then you reflow and remove this override, and your element does its animation again.Gamaliel
L
97

Just set the animation property via JavaScript to "none" and then set a timeout that changes the property to "", so it inherits from the CSS again.

Demo for Webkit here: http://jsfiddle.net/leaverou/xK6sa/ However, keep in mind that in real world usage, you should also include -moz- (at least).

Liegnitz answered 10/6, 2011 at 7:34 Comment(7)
Thanks Lea. Almost there :), If I change your animation to run only once I don't quite get the same effect. When I click, the animation doesn't start over again.Vexation
Thanks a lot! - But unfortunately i cannot get it to work in Safari. Chrome, Edge and Firefox are working as expected. i use following code: var anim = jQuery(mutation.target).find(".background.background-image:first").get(0); anim.style.WebkitAnimation = 'none'; anim.style.animation = 'none'; setTimeout(function() { anim.style.WebkitAnimation = ''; anim.style.animation = ''; }, 10); }Perception
Not good enough, because sometimes a flaw could be seen if timeout is too long, and the effect isn't taken if timeout is too short. Not recommended if you need to restart animation when it is still playing.Levator
It's 2019 now, vendor prefix for this should no longer be necessary.Cookhouse
To avoid the timeout problems described by @Eric, you can call void element.offsetWidth; to force a reflow in between the animation property changes instead of using a timeout.Ilex
How to achieve it for multiple buttons ? how to start animation for multiple buttons ?Lighten
As of 2024, this should no longer be the accepted answer. The Element.getAnimations() or Document.getAnimations() methods are much cleaner. V. Rubinetti's answer should now be the accepted answer.Hebron
U
22

@ZachB's answer about the Web Animation API seems like "right"™ way to do this, but unfortunately seems to require that you define your animations through JavaScript. However it caught my eye and I found something related that's useful:

Element.getAnimations() and Document.getAnimations()

The support for them is pretty good as of 2021.

In my case, I wanted to restart all the animations on the page at the same time, so all I had to do was this:

const replayAnimations = () => {
  document.getAnimations().forEach((anim) => {
    anim.cancel();
    anim.play();
  });
};

But in most cases people will probably want to select which animation they restart...

getAnimations returns a bunch of CSSAnimation and CSSTransition objects that look like this:

animationName: "fade"
currentTime: 1500
effect: KeyframeEffect
  composite: "replace"
  pseudoElement: null
  target: path.line.yellow
finished: Promise {<fulfilled>: CSSAnimation}
playState: "finished"
ready: Promise {<fulfilled>: CSSAnimation}
replaceState: "active"
timeline: DocumentTimeline {currentTime: 135640.502}

# ...etc

So you could use the animationName and target properties to select just the animations you want (albeit a little circuitously).


EDIT

Here's a handy function that might be more compatible using just Document.getAnimations, with TypeScript thrown in for demonstration:

// restart animations on a given dom element
export const restartAnimations = (element: Element): void => {
  for (const animation of document.getAnimations()) {
    if (
      animation.effect instanceof KeyframeEffect &&
      element.contains(animation.effect.target)
    ) {
      animation.cancel();
      animation.play();
    }
  }
};
Uda answered 20/4, 2021 at 17:10 Comment(4)
If only this was supported across all browsers, Safari always late to the show..Typewriter
Note that the support for Element.getAnimations() and Document.getAnimations() is different. The latter seems to be supported by Safari, and everything but IE. So to be more reliable, use the latter, and you'll just have to do some extra manual filtering.Uda
As of early 2024, I think it's safe to use either in any (major) browser. It's also curious that MDN compat data says that Element was supported in Safari before Document. I think that's wrong, but I only have my own anecdote and memory to go off of.Uda
So clean. As of 2024, this should be the accepted answer. Finally no more hacks. The cancel() method worked for me even with simple transitions applied via CSS in an external .css file. That seems consistent with MDN's documentation, which states that the array returned by getAnimation() "includes CSS Animations, CSS Transitions, and Web Animations."Hebron
S
14
  1. Implement the animation as a CSS descriptor
  2. Add the descriptor to an element to start the animation
  3. Use a animationend event handler function to remove the descriptor when the animation completes so that it will be ready to be added again next time you want to restart the animation.

HTML

<div id="animatedText">
    Animation happens here
</div>

<script>
  function startanimation(element) {
    element.classList.add("animateDescriptor");
    element.addEventListener( "animationend",  function() {
      element.classList.remove("animateDescriptor");    
    } );
  }
</script>

<button onclick="startanimation( 
  document.getElementById('animatedText') )">
    Click to animate above text
</button>

CSS

@keyframes fadeinout {  
    from { color: #000000; }    
    25% {color: #0000FF; }  
    50% {color: #00FF00; }      
    75% {color: #FF0000; }  
    to { color : #000000; } 
}   
        
.animateDescriptor {    
    animation: fadeinout 1.0s;  
}   

Try it here: jsfiddle

Sunrise answered 19/8, 2018 at 1:51 Comment(1)
Using the animationend event is a good idea, although you don't really want to add a new eventlistener ever time the animation is run. Just add it to the div once.Reopen
H
7

If you have a class for CSS3 animation, for example .blink, then you can removeClass for some element and addClass for this element thought setTimeout with 1 millisecond by click.

$("#element").click(function(){
   $(this).removeClass("blink");

   setTimeout(function(){
     $(this).addClass("blink);
  },1 // it may be only 1 millisecond, but it's enough
});
Halfbeak answered 28/3, 2017 at 15:31 Comment(2)
Doesn't seem to be reliable. Didn't work for me with a 1 ms timer, starts working with higher values (in Firefox 67.)Jequirity
@FabienSnauwaert thanks for your comment, you can be right, because I tested I guess on ~FF 50Halfbeak
A
7

You can also use display property, just set the display to none.

display:none;

and the change backs it to block (or any other property you want).

display:block;

using JavaScript.

and it will work amazingly.

Arjuna answered 2/5, 2020 at 6:33 Comment(0)
F
4

There is an answer on MDN, which is similar to the reflow approach:

<div class="box">
</div>

<div class="runButton">Click me to run the animation</div>
@keyframes colorchange {
  0% { background: yellow }
  100% { background: blue }
}

.box {
  width: 100px;
  height: 100px;
  border: 1px solid black;
}

.changing {
  animation: colorchange 2s;
}
function play() {
  document.querySelector(".box").className = "box";
  window.requestAnimationFrame(function(time) {
    window.requestAnimationFrame(function(time) {
      document.querySelector(".box").className = "box changing";
    });
  });
}
Faunie answered 26/6, 2019 at 20:13 Comment(4)
Two nested calls to requestAnimationFrame are not guaranteed to be adequate, and there's no technical reason they have to be adequate. This looks functionally better than setTimeout but in actuality it isn't.Scholl
@AdamLeggett Can you elaborate? I have recently had to look into doing this.Faunie
I am not as well versed in browser architecture as I'd like to be, but my understanding is that restarting the animation depends on the layout engine, which is in a different thread from the rendering engine. requestAnimationFrame waits for the rendering engine. The most technically correct answer is to use the animate function; the second most technically correct is unfortunately void element.offsetWidth.Scholl
This is awesome, adding a CSS class that contains the animation propertyPerspicacious
M
4

The Animation API gives you full control over when and what to play, and is supported by all modern browsers (Safari 12.1+, Chrome 44+, Firefox 48+, Edge 79+) .

const effect = new KeyframeEffect(
  el, // Element to animate
  [ // Keyframes
    {transform: "translateY(0%)"}, 
    {transform: "translateY(100%)"}
  ], 
  {duration: 3000, direction: "alternate", easing: "linear"} // Keyframe settings
);

const animation = new Animation(effect, document.timeline);

animation.play();

Demo: https://jsfiddle.net/cstz9L8v/

References:

Mailemailed answered 9/11, 2020 at 20:41 Comment(0)
T
4

If you create two identical sets of keyframes, you can "restart" the animation by swapping between them:

function restart_animation(element) {
  element.classList.toggle('alt')
}
@keyframes spin1 {
  to { transform: rotateY(360deg); }
}
@keyframes spin2 {
  to { transform: rotateY(360deg); }
}
.spin {
  animation-name: spin1;
  animation-duration: 2s;
}
.alt {
  animation-name: spin2;
}

div {
  width: 100px;
  background: #8CF;
  padding: 5px;
}
<div id=_square class=spin>
  <button onclick="restart_animation(_square)">
    Click to restart animation
  </button>
</div>
Tabshey answered 7/6, 2022 at 2:35 Comment(1)
Wow this is actually quite clever! Simplest and working solution for me thanks!Benghazi
S
2

On this page you can read about restarting the element animation: Restart CSS Animation (CSS Tricks)

Here is my example:

<head>
    <style>
        @keyframes selectss
        {
            0%{opacity: 0.7;transform:scale(1);} 
            100%{transform:scale(2);opacity: 0;}
        }
    </style>
    <script>
        function animation()
        {
            var elm = document.getElementById('circle');
            elm.style.animation='selectss 2s ease-out';
            var newone = elm.cloneNode(true);
            elm.parentNode.replaceChild(newone, elm);
        }
    </script>
</head>
<body>
    <div id="circle" style="height: 280px;width: 280px;opacity: 0;background-color: aqua;border-radius: 500px;"></div>
    <button onclick="animation()"></button>
</body>

But if you want to you can just remove the element animation and then return it:

function animation()
{
    var elm = document.getElementById('circle');
    elm.style.animation='';
    setTimeout(function () {elm.style.animation='selectss 2s ease-out';},10)
}
Saleratus answered 17/4, 2016 at 12:20 Comment(0)
B
1

While still not ideal you could just restate your @keyframes with a different name, then just change the animation name when you want to run it again. This has the benefit of not needing JS in some use cases, like on hover or focus, etc:

@keyframes animate {
    0% {
        opacity: 0,
    }

    100% {
        opacity: 1;
    }
}

@keyframes animate_again {
    0% {
        opacity: 0,
    }

    100% {
        opacity: 1;
    }
}

div {
    animation: animate .2s ease-in-out;
}
div:hover {
    animation-name: animate_again;
}
Bretbretagne answered 4/9, 2023 at 19:37 Comment(0)
A
0

Create a second "keyframe@" which restarts you animation, only problem with this you cannot set any animation properties for the restarting animation (it just kinda pops back)

HTML

<div class="slide">
    Some text..............
    <div id="slide-anim"></div>
</div><br>
    <button onclick="slider()"> Animation </button>
    <button id="anim-restart"> Restart Animation </button>
<script>
    var animElement = document.getElementById('slide-anim');
    document.getElementById('anim-restart').addEventListener("mouseup", restart_slider);
    
    function slider() {
        animElement.style.animationName = "slider";             // other animation properties are specified in CSS
    }
    function restart_slider() {
        animElement.style.animation = "slider-restart";         
    }
</script>

CSS

.slide {
    position: relative;
    border: 3px black inset;
    padding: 3px;
    width: auto;
    overflow: hidden;
}
.slide div:first-child {
    position: absolute;
    width: 100%;
    height: 100%;
    background: url(wood.jpg) repeat-x;
    left: 0%;
    top: 0%;            
    animation-duration: 2s;
    animation-delay: 250ms;
    animation-fill-mode: forwards;
    animation-timing-function: cubic-bezier(.33,.99,1,1); 
}

@keyframes slider {
    to {left: 100%;}
}

@keyframes slider-restart {
    to {left: 0%;}
}
Azoic answered 2/1, 2018 at 7:28 Comment(0)
A
0

Note that with React, clearing the animation like this, a codesandbox I found helps.

Example I used in my code:

function MyComponent() {
  const [shouldTransition, setShouldTransition] = useState(true);

  useEffect(() => {
    setTimeout(() => {
      // in my code, I change a background image here, and call this hook restart then animation,
      // which first clears the animationName
      setShouldTransition(false);
    }, timeout * 1000);
  }, [curr]);

  useEffect(() => {
    // then restore the animation name after it was cleared
    if (shouldTransition === false) {
      setShouldTransition(true);
    }
  }, [shouldTransition]);
  return (
    <div
      ref={ref2}
      style={{
        animationName: shouldTransition ? "zoomin" : "",
      }}
    />
  );
}
Adventuresome answered 13/2, 2022 at 5:9 Comment(0)
W
0
setInterval(() => {
    $('#XMLID_640_').css('animation', 'none')

    setTimeout(() => {
        $('#XMLID_640_').css('animation', '')
    }, 3000)
}, 13000)
Waller answered 20/3, 2022 at 3:58 Comment(2)
Please add an explanation to your answer explaining why/how it answers the question.Delectable
So this creates an interval where the CSS animation property is removed every 13 seconds, and after those 13 seconds, the animation is added 3 seconds later. So it's a continuous animation cycle? 😅Guiltless
G
0

I found out a simple solution today. Using the example provided in this answer, you can just append the element again to the body:

function resetAnimation() {
    let element = document.getElementById('animated');
    document.body.append(element);
}
#animated {
    position: absolute;
    width: 50px;
    height: 50px;
    background-color: LightSalmon;
    animation: bounce 3s ease-in-out infinite;
}
@keyframes bounce {
    0% {left: 0;}
    50% {left: calc(100% - 50px);}
    100% {left: 0;}
}
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="animated"></div>
<button onclick="resetAnimation()">Reset</button>
</body>
</html>

Using Chrome's developer tools, the append does not actually append the element to the body and just replace it, probably because the same reference to the element is used.

Gard answered 19/11, 2022 at 21:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.