I have a CSS animation that gets applied to a HTML element with a CSS class. I also have an event listener attached to the animationend (it's a modern Windows app so the browser is IE11 only). What I'm seeing is that sometimes the event gets fired and sometimes it doesn't. Regardless of the event firing I can always see it visually animating. It looks like some kind of race condition to me.
I have searched the web trying to understand what could cause this event to not get fired but I haven't found any satisfying results. I found the following on MDN:
Note: The transitionend event doesn't fire if the transition is aborted because the animating property's value is changed before the transition is completed.
UPDATE1: transitionend has nothing to do with animationend so this info is unrelated.
I'm not sure if the same is applicable for CSS animations. Does anyone have any ideas as to what can cause this? If there was any event that could detect that an animation was aborted that could also be a useful workaround.
UPDATE2: The current workaround that I'm using:
element.addEventListener("animationstart", function () {
setTimeout(function () {
// do stuff
}, animationDuration);
});
Is it possible that your animations aren't ending?
Check the documentation for the animationcancel event:
https://developer.mozilla.org/en-US/docs/Web/Events/animationcancel
However, it looks like browser support may be spotty ("onanimationcancel" in HTMLElement.prototype returns false for me in Chrome 67).
It seems that using a setTimeout hack may be necessary for the time being.
You can use this function
function onAnimationEnd(element, handler) {
//- Create local variables
var events = 'animationend transitionend webkitAnimationEnd oanimationend MSAnimationEnd webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd';
//- Bind event to element
$(element).on(events, handler)
//- Return received element
return element
}
This function binds an event when both animation or transition ends.
Also make sure that the animated element is not being deleted or moved while the animation is running.
With IE11, you can't necessarily assume that the JS event binding will occur before the CSS animation starts or ends. This type of issue is intermittent, and frequency/onset will depend on certain factors.
If the CSS animation-duration is low and the animation starts immediately after the stylesheet is ready, then it's possible that the JS event binding will not be applied in time
A solid workaround I've found is to use JS to invoke the CSS animation using a className. i.e., Only apply the CSS animation-name property when the className has been applied via JS.
FWIW, this is still a problem. In some cases (so far I've only reproduced it on mobile) certain elements with a well-defined animation don't always fire the animationend event.
EDIT: Turns out the issue is with animating after content. Safari just doesn't reliably handle it. I ended up making a distinct strikethrough div and animating that.
function animateCSS(element, animationName, callback) {
const node = typeof element == 'string' ? document.querySelector(element) : element;
node.classList.add('animated', animationName);
node.onanimationend = handleAnimationEnd;
console.log('animateCSS', node, animationName);
function handleAnimationEnd() {
console.log('handleAnimationEnd', node, 'remove:', animationName);
node.classList.remove('animated', animationName);
node.onanimationend = null;
if (typeof callback === 'function') callback(node);
}
}
.strike {
color: #e402b3;
position: relative;
}
.strike:after {
content: ' ';
position: absolute;
top: 50%;
left: 2%;
height: 2px;
background: #e402b3;
animation: strike 0.7s linear 0s 1 forwards;
-webkit-animation: strike 0.7s linear 0s 1 forwards;
}
#keyframes strike {
0% {
width: 0%;
color: #000;
}
100% {
width: 96%;
color: #e402b3;
}
}
#-webkit-keyframes strike {
0% {
width: 0%;
color: #000;
}
100% {
width: 96%;
color: #e402b3;
}
}
Related
This question already has answers here:
Restart animation in CSS3: any better way than removing the element?
(14 answers)
Closed 1 year ago.
So in this example, if I don't nest the
box.style.animation = 'myanimation 3s'
inside of the setTimeout, then the code doesn't work. But when I run it like this, with a timeout delay of ZERO, it works. Originally I thought that maybe it was an execution timing error, so I set the Timeout delay to 50, but I got curious and tried lower amounts until I got to zero, and it still ran. Essentially I was just trying to implement an animation every time the element is clicked. Is there a better/safer workaround for this? Also, for curiosity, what is the difference between running that line of code directly after the 'if' statement versus nesting it in a setTimeout with a delay of 0? I'm using Firefox with Ubuntu LTS 20.04
edit: I should add, if I don't nest inside the setTimeout function, the animation will run the first time, but not any subsequent time. But when I run the code as shown, it runs everytime.enter image description here
const box = document.querySelector('.box')
box.addEventListener('click', function() {
if(box.style.animation){
box.style.animation = ''
}
setTimeout(function() {
box.style.animation = 'myanimation 3s'
}, 0)
})
EDIT: Another answer I came across on reddit, is to listen for an animationend event, such as:
const box = document.querySelector('.box')
box.addEventListener('click', function() {
box.style.animation = 'myanimation 3s'
})
box.addEventListener('animationend', function() {
box.style.animation = ''
})
The reason why it seems like nothing is happening when you don't use the timeout is that modern browsers optimize away the first statement completely, in order to prevent unnecessary operations from happening that would have a negative effect on performance.
The browser notices that you set the animation property to an empty string, just to set it to another value in the line below. That would cause a reflow, and in many cases that wouldn't be what you want. In this case, trying to reset a CSS animation, it is exactly what you want, though.
So you need to tell the browser to intentionally cause a reflow. One way to do that is using setTimeout, so the first expression will not be optimized away. Another way is to cause reflow by doing something that will itself inherently cause a reflow. offsetLeft, offsetTop, offsetWidth and offsetHeight are such a properties that cause a reflow in order to report back accurate values when they are accessed. It's enough to simply access them, without putting the values into a variable or anything:
const box = document.querySelector('.box')
box.addEventListener('click', function() {
if (box.style.animation) {
box.style.animation = ''
}
box.offsetLeft; // this forces a reflow, just like setTimeout
box.style.animation = 'myanimation 3s'
})
.box {
background: red;
width: 120px;
height: 120px;
position: relative;
}
.box::after {
content: "click me!";
color: #fff;
display: block;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%)
}
#keyframes myanimation {
from {
transform: translateY(0)
}
50% {
transform: translateY(1em)
}
to {
transform: translateY(0);
}
}
<div class="box">
</div>
I am trying to make a kind of template, for a dashboard page, where when a dashboard button is clicked, something is added to the DOM.
I was trying to template this, so that when someone makes a new dashboard for example, he has the option, to specify in CSS an animation that should run on each button when clicked. If an animation is defined on the button, the actual loading of the element should be delayed until the animation completes.
Now, if I actually specify an animation in css, everything works fine, because I am delaying the logical code with a callback on the animations end. My problem is, that I can't achieve the same, when there is no animation set to the element.
What I would want is something simmilar:
function buttonClick($button) {
$button.addClass('activated');
$button.one('animationend', function() {
// ... run the logic here
});
if (...no animation specified in activated class) {
// ... run the logic here
}
}
NOTE: I am using jQuery here, if there is a method specific in jQuery for this, that would also be okay for me, but a plain javascript method would be fine as well. I heard about the jQuery(":animated") selector, but when I was testing it, it seems that it only works for animations started with jQuery itself, and not with CSS3.
As you seem to use animation CSS for your animations (given that you use the animationend event), you could use getComputedStyle to verify the content of the animation-name CSS property.
Here is a demo with two buttons: one triggers an animation on click, while the other doesn't:
$("button").click(function () {
buttonClick($(this));
});
function hasAnimation($button) {
return getComputedStyle($button[0], null)["animation-name"] != "none";
}
function onEndAnimation($button) {
$button.removeClass('activated');
console.log("animation complete on button " + $button.text());
}
function buttonClick($button) {
$button.addClass('activated');
$button.one('animationend', () => onEndAnimation($button));
if (!hasAnimation($button)) onEndAnimation($button);
}
#yes.activated {
animation-duration: 1s;
animation-name: grow;
}
#keyframes grow {
from { width: 50px; }
50% { width: 100px; }
to { width: 50px; }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button id="yes" style="width: 50px">Yes</button><br>
<button id = "no" style="width: 50px">No</button>
This question was asked before, but the answer uses jQuery, here.
So, I am going to tweak the question to specifically ask for a native solution, to minimize dependencies.
Let's say hypothetically, you have a <div> and that <div> is in mid-transition of its opacity value and top value. How would I get the value of both of those properties mid-transition using native JavaScript?
It is very easy to port the jQuery script from the linked thread into its vanilla JavaScript equivalent and below is a sample. The output is printed on the right side (output#op element) once timer expires.
All that we are doing is the following:
Attach two event handlers to the element which triggers the transition (sometimes the triggering element can be different from the one that has animation). In the other thread, the element that is triggering the transition and the one that is being transitioned is the same. Here, I have put it on two different elements just for a different demo.
One event handler is for mouseover event and this creates a timer (using setTimeout) which gets the opacity and top value of the element that is being transitioned upon expiry of timer.
The other event handler is for mouseleave event to clear the timer when the user has hovered out before the specific point at which we need the opacity and top value to be obtained.
Getting the opacity and top value of the element that is being transitioned can be obtained by using the window.getComputedStyle method.
Unlike the demo in the other thread (which uses setInterval), here I have used setTimeout. The difference is that setInterval adds an interval and so the function is executed every x seconds whereas the function passed to setTimeout is executed only once after x seconds. You can use whichever fits your needs.
var wrap = document.querySelector('.wrapper'),
el = document.querySelector('.with-transition'),
op = document.querySelector('#op');
var tmr;
wrap.addEventListener('mouseenter', function() {
tmr = setTimeout(function() {
op.innerHTML = 'Opacity: ' + window.getComputedStyle(el).opacity +
', Top: ' + window.getComputedStyle(el).top;
}, 2500);
});
wrap.addEventListener('mouseleave', function() {
clearTimeout(tmr);
});
.wrapper {
position: relative;
height: 400px;
width: 400px;
background: yellowgreen;
}
.with-transition {
position: absolute;
top: 0px;
left: 100px;
width: 200px;
height: 100px;
background: yellow;
opacity: 0;
transition: all 5s linear;
}
.wrapper:hover .with-transition {
top: 300px;
opacity: 1;
}
output {
position: absolute;
top: 50px;
right: 50px;
}
<div class='wrapper'>
<div class='with-transition'></div>
</div>
<output id='op'></output>
The answer referenced in the duplicate question is easily modified to NOT use jquery. There is no black magic happening there.
The real question is why would you want to do this?
If You need control over a transition just impliment the partial transition with javascript, do what you need, then complete the transition.
I have an element with a transition applied to it. I want to control the transition by adding a class to the element which causes the transition to run. However, if I apply the class too quickly, the transition effect does not take place.
I'm assuming this is because the .shown is placed onto the div during the same event loop as when .foo is placed onto the DOM. This tricks the browser into thinking that it was created with opacity: 1 so no transition is put into place.
I'm wondering if there is an elegant solution to this rather than wrapping my class in a setTimeout.
Here's a snippet:
var foo = $('<div>', {
'class': 'foo'
});
foo.appendTo($('body'));
setTimeout(function(){
foo.addClass('shown');
});
.foo {
opacity: 0;
transition: opacity 5s ease;
width: 200px;
height: 200px;
background-color: red;
}
.foo.shown {
opacity: 1;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Actually, the point is not about the setTimeout, but about how the element is rendered.
The CSS transition will only appear if the element is rendered with a property value, and then this property is changed.
But once you append the element, it does not mean that it was rendered. Simply adding a setTimeout is not enough. Thought it may work for you, in some browser versions it won't work! (Mostly Firefox)
The point is about the element's render time. Instead of setTimeout, you can force a DOM render by requesting a visual style property, and then changing the class:
var foo = $('<div>', {
'class': 'foo'
});
foo.appendTo($('body'));
//Here I request a visual render.
var x = foo[0].clientHeight;
//And it works, without setTimeout
foo.addClass('shown');
.foo {
opacity: 0;
transition: opacity 5s ease;
width: 200px;
height: 200px;
background-color: red;
}
.foo.shown {
opacity: 1;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
When you do DOM manipulation that that javascript relies on immediately afterwards, you need to pause javascript execution briefly in order to allow rendering to catch up, since that will be done asynchronously. All a blank setTimeout does is move the code within to the end of the current execution pipeline. The browser must complete rendering the new layout before it will obey a trigger for your transition so the setTimeout is a good idea and in my opinion the most elegant solution.
I have a function that should scroll a user back to a search input at the top of the page, and then place focus on it (so the cursor is blinking). For some reason, it seems to apply the focus to the search input first. This creates a very quick jump/spazzy movement of the page to the search bar, jumps back to the bottom, and then scrolls up slowly.
The Javascript:
function goToSearch(){
$('html,body').animate({scrollTop: $('#search').offset().top},'medium');
$('#search').focus()
}
The HTML:
<input type="text" id="search" placeholder="search">
...
Search
I've tried setting .delay() functions to no avail; it seems to always apply the .focus() first. Why is this happening?
"Why is this happening?"
The animation effect is asynchronous. That is, the .animate() function returns immediately after "scheduling" the animation (so to speak) and execution continues immediately with the next statement - in your case, the .focus() statement. The actual animation will take place after the current JS completes.
Fortunately the .animate() method provides an option for you to pass a callback function that will be called when the animation is complete, so you can do the focus within that callback function:
$('html,body').animate({scrollTop: $('#search').offset().top},'medium', function(){
$('#search').focus();
});
You should call the focus function when the animation is complete, like so:
function goToSearch(){
$('html,body').animate({scrollTop: $('#search').offset().top},'medium',function(){
$('#search').focus();
});
}
If you found this question like me, you were probably looking for a CSS related issue coupled with the js focus() and you might think you're out of luck. Well, think again - the good news is that there IS a way to have a callback when a CSS animation or transition ends and then fire your event.
You can make use of jQuery's one method and use either webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend for transitions or webkitAnimationEnd oanimationend msAnimationEnd animationend for animations.
An example:
JS:
var myButton = $('#button'),
myBox = $('#box');
myButton.click(function () {
myBox.addClass('change-size');
myBox.one('webkitAnimationEnd oanimationend msAnimationEnd animationend',
function(e) {
// code to execute after animation ends
myBox.removeClass('change-size');
});
});
CSS:
.box {
width: 100px;
height: 100px;
background: hotpink;
}
#keyframes growBox {
to {
width: 300px;
height: 200px;
}
}
.change-size {
animation: growBox 3s linear 0s 1 normal;
}
The solution is not mine, I just found it after a couple of wasted hours, and I'm posting it in case you find this question first.
Source: http://blog.teamtreehouse.com/using-jquery-to-detect-when-css3-animations-and-transitions-end