Can I wait for multiple CSS animations using JS? - javascript

We have a way to detect when an animation ends using JS:
const element = $('#animatable');
element.addClass('being-animated').on("animationend", (event) => {
console.log('Animation ended!');
});
#keyframes animateOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#keyframes animatePosition {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(0, 15px, 0);
}
}
#animatable.being-animated {
animation: animateOpacity 1s ease 0s forwards, animatePosition 2s ease 0s forwards;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="animatable">I'm probably being animated.</div>
And as you can see, JS, rightfully, because I'm hooked to the animationend event tells me "yup, an animation is done" but isn't aware of what's coming after and I'm missing the second one.
Isn't there an animation queue? Surely CSS has to register these things somewhere in the system before they're fired and I could peak inside.

Disclaimer: I don't think jQuery is important to answer this question and would hurt both load and runtime performance if others choose to rely on this code after seeing this answer. So, I will be answering with vanilla JavaScript to help as many people as I can with this, but if you want to use jQuery, you can still apply the same concepts.
Answer: There isn't an animation queue, but you could make your own.
For example, you could link data about animations to your target element using a closure, and/or a Map (In the snippet below, I actually used a WeakMap in an attempt to help garbage collection). If you save animation states as true when they are completed, you could check and eventually fire a different callback when all are true, or dispatch a custom event of your own. I used the custom event approach, because it's more flexible (able to add multiple callbacks).
The following code should additionally help you avoid waiting for ALL animations in those cases where you only actually care about a couple specific ones. It should also let you handle animation events multiple times and for multiple individual elements (try running the snippet and clicking the boxes a few times)
const addAnimationEndAllEvent = (() => {
const weakMap = new WeakMap()
const initAnimationsObject = (element, expectedAnimations, eventName) => {
const events = weakMap.get(element)
const animationsCompleted = {}
for (const animation of expectedAnimations) {
animationsCompleted[animation] = false
}
events[eventName] = animationsCompleted
}
return (element, expectedAnimations, eventName = 'animationendall') => {
if (!weakMap.has(element)) weakMap.set(element, {})
if (expectedAnimations) {
initAnimationsObject(element, expectedAnimations, eventName)
}
// When any animation completes...
element.addEventListener('animationend', ({ target, animationName }) => {
const events = weakMap.get(target)
// Use all animations, if there were none provided earlier
if (!events[eventName]) {
initAnimationsObject(target, window.getComputedStyle(target).animationName.split(', '), eventName)
}
const animationsCompleted = events[eventName]
// Ensure this animation should be tracked
if (!(animationName in animationsCompleted)) return
// Mark the current animation as complete (true)
animationsCompleted[animationName] = true
// If every animation is now completed...
if (Object.values(animationsCompleted).every(
isCompleted => isCompleted === true
)) {
const animations = Object.keys(animationsCompleted)
// Fire the event
target.dispatchEvent(new CustomEvent(eventName, {
detail: { target, animations },
}))
// Reset for next time - set all animations to not complete (false)
initAnimationsObject(target, animations, eventName)
}
})
}
})()
const toggleAnimation = ({ target }) => {
target.classList.toggle('being-animated')
}
document.querySelectorAll('.animatable').forEach(element => {
// Wait for all animations before firing the default event "animationendall"
addAnimationEndAllEvent(element)
// Wait for the provided animations before firing the event "animationend2"
addAnimationEndAllEvent(element, [
'animateOpacity',
'animatePosition'
], 'animationend2')
// Listen for our added "animationendall" event
element.addEventListener('animationendall', ({detail: { target, animations }}) => {
console.log(`Animations: ${animations.join(', ')} - Complete`)
})
// Listen for our added "animationend2" event
element.addEventListener('animationend2', ({detail: { target, animations }}) => {
console.log(`Animations: ${animations.join(', ')} - Complete`)
})
// Just updated this to function on click, so we can test animation multiple times
element.addEventListener('click', toggleAnimation)
})
.animatable {
margin: 5px;
width: 100px;
height: 100px;
background: black;
}
#keyframes animateOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#keyframes animatePosition {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(0, 15px, 0);
}
}
#keyframes animateRotation {
100% {
transform: rotate(360deg);
}
}
.animatable.being-animated {
animation:
animateOpacity 1s ease 0s forwards,
animatePosition 1.5s ease 0s forwards,
animateRotation 2s ease 0s forwards;
}
<div class="animatable"></div>
<div class="animatable"></div>

#BDawg's awesome snippet is more flexible and thorough, it certainly deserves to be the accepted answer. That said, I was inspired to see if a less verbose approach was feasible. Here's what I came up with.
It's pretty self-explanitory, but basically the concept is that all the animation properties' indexes correlate, and we can use that to find the name of the animation that finishes last.
const getFinalAnimationName = el => {
const style = window.getComputedStyle(el)
// get the combined duration of all timing properties
const [durations, iterations, delays] = ['Duration', 'IterationCount', 'Delay']
.map(prop => style[`animation${prop}`].split(', ')
.map(val => Number(val.replace(/[^0-9\.]/g, ''))))
const combinedDurations = durations.map((duration, idx) =>
duration * iterations[idx] + delays[idx])
// use the index of the longest duration to select the animation name
const finalAnimationIdx = combinedDurations
.findIndex(d => d === Math.max(...combinedDurations))
return style.animationName.split(', ')[finalAnimationIdx]
}
// pipe your element through this function to give it the ability to dispatch the 'animationendall' event
const addAnimationEndAllEvent = el => {
const animationendall = new CustomEvent('animationendall')
el.addEventListener('animationend', ({animationName}) =>
animationName === getFinalAnimationName(el) &&
el.dispatchEvent(animationendall))
return el
}
// example usage
const animatable = document.querySelector('.animatable')
addAnimationEndAllEvent(animatable)
.addEventListener('animationendall', () => console.log('All animations have finished'))
.animatable {
width: 50px;
height: 50px;
background-color: red;
position: relative;
left: 0;
animation: 1.5s slidein, 1s fadein;
}
#keyframes slidein {
0% { left: 100vw; }
100% { left: 0; }
}
#keyframes fadein {
0% { opacity: 0; }
100% { opacity: 1; }
}
<div class="animatable"></div>

First technique:
Add a class to an element, then handle every animation and wait for them to end, no matter what. This is the common way to do things where you trigger animations by classes.
As per Kaiido's comment and pointing out, this waits for every single animation, no matter how long to finish. This was the motivation behind all of this: create a nice animation and make JS aware of it (no matter how complex / long) finishing it so you could then chain other things.
If you don't do this, you might have a nice animation running and suddenly being cut by something else and...that's bad.
const triggerAnimationWithClass = (classToAdd, toWhat) => {
const element = document.querySelector(toWhat);
/**
* Initialize the count with 1, because you'll always have at least one animation no matter what.
*/
let animationCount = 1;
return new Promise((resolve, reject) => {
element.addEventListener('animationend', (event) => {
if((window.getComputedStyle(element).animationName).split(',').length - animationCount === 0) {
/**
* Remove the current function being hooked once we're done. When a class gets added that contains any N number of animations,
* we're running in a synchronous environment. There is virtually no way for another animation to happen at this point, so, we're
* surgically looking at animations that only happen when our classToAdd gets applied then hooking off to not create conflicts.
*/
element.removeEventListener('animationend', this);
const animationsDonePackage = {
'animatedWithClass': classToAdd,
'animatedElement': toWhat,
'animatedDoneTime': new Date().getTime()
};
resolve(animationsDonePackage);
} else {
animationCount++;
}
});
element.classList.add(classToAdd);
});
}
This handles multiple classes being added. Let's assume that from the outside, someone adds yet another class at the same time (weird, but, let's say it happens) you've added yours. All the animations on that element are then treated as one and the function will wait for all of them to finish.
Second technique:
Based on #B-Dawg's answer. Handle a set of animations, based on name (CSS animation names), not class, please read the after-word:
const onAnimationsComplete = ({element, animationsToLookFor}) => {
const animationsMap = new WeakMap();
if(!animationsMap.has(element)) {
const animationsCompleted = {};
for(const animation of animationsToLookFor) {
animationsCompleted[animation] = false;
}
animationsMap.set(element, animationsCompleted);
}
return new Promise((resolve, reject) => {
// When any animation completes...
element.addEventListener('animationend', ({target, animationName, elapsedTime}) => {
const animationsCompleted = animationsMap.get(target);
animationsCompleted[animationName] = true;
// If every animation is now completed...
if(Object.values(animationsCompleted).every(isCompleted => isCompleted === true)) {
const animations = Object.keys(animationsCompleted);
// Reset for next time - set all animations to not complete (false)
animations.forEach(animation => animationsCompleted[animation] = false);
//Remove the listener once we're done.
element.removeEventListener('animationend', this);
resolve({
'animationsDone': animationsToLookFor
});
}
});
});
};
This has a bug. Assuming that a new animation comes from, say, maybe a new class, if it's not put in the animationsToLookFor list, this never resolves.
Trying to fix it but if we're talking about a precise list of animations you're looking for, this is the go-to.

Related

How can I know if a div is rotating?

I have a div, I can rotate it to some degree by using
rotateX(degree)
In css3 I set
transition: 2s
The div will begin rotating and stop in 2s. Here is my problem:
How can I get if the div is rotating or stopped?
Thanks.
Aÿlo already gave a good answer. But, animationstart method may have a problem of compatibility.
animationstart useage value: 96.77%
If compatibility is important, I think it would be good to add the getComputedStyle method as a fallback.
getComputedStyle useage value: 99.72%
Using the getComputedStyle fallback method, you can write the code as below.
const DEGREE_BY_ONE_SEC = 90
const DEGREE_SEC = 2
let hasRotated = false
div.addEventListener('transitionend', () => hasRotated = true); // Aÿlo's code
window.setTimeout(() => {
const rotationDegrees = getRotationDegrees(element) // Ref: https://stackoverflow.com/a/4351437/11618421
if (rotationDegrees === DEGREE_BY_ONE_SEC * DEGREE_SEC) {
hasRotated = true
return
}
}, DEGREE_SEC)
There are some events related to the transitions that you may use.
Here's a simple live demo to get you started:
const rotatingDiv = document.getElementById('rotating');
/** listen for transition start */
rotatingDiv.addEventListener('transitionstart', () => console.log('transition started..'));
/** transition is running */
rotatingDiv.addEventListener('transitionrun', () => console.log('transition running....'));
/** listen for transition end */
rotatingDiv.addEventListener('transitionend', () => console.log('transition finished.'));
/** for demo purposes only */
#rotating {
width: 100px;
height: 100px;
border: 2px solid red;
transition: transform 2s 0s ease;
}
#rotating:hover {
transform: rotate(180deg); /** change this per your requirement */
}
.as-console-wrapper {
max-height: 80px!important;
}
<div id="rotating">
<div>
The above demo showcases the events related to CSS transitions. If you'd need to use animation instead, then you should look into animation related events like the animationend event.
If it helps, you can leverage the 'transitioned' event, to check when the element's transition has stopped, like so:
let hasRotated = false
div.addEventListener('transitionend', () => hasRotated = true);
More on the event here:
https://developer.mozilla.org/en-US/docs/Web/API/Element/transitionend_event
You can use JavaScript's setTimeout function, which waits some milliseconds before calling a function.
setTimeout(function() {
console.log("Div has stopped rotating.");
}, 2000)

Fade In and Fade Out using pure Javascript in a simple way

I've been trying to create a fadeIn & fadeOut animation using VanillaJS in my project but I literally don't understand what's the problem. I'm using SCSS. I made it simple for you.
I tried visibility but it didn't work too. like it appears e.g. for 200ms but then immediately disappears. In another way of explanation, it appears whenever I click on it (stable) and then goes away after 200ms (unstable).
const fade = () => {
const box = document.querySelector('#box');
box.classList.toggle('fade');
};
document.querySelector('#fadebtn').addEventListener('click', fade);
#box {
width: 70px;
height: 50px;
background: #FD7A6B;
display: none;
opacity: 0;
-webkit-transition: 200ms ease-in-out;
-moz-transition: 200ms ease-in-out;
-o-transition: 200ms ease-in-out;
transition: 200ms ease-in-out;
}
#box.fade {
display: block !important;
opacity: 1 !important;
}
// I also tried this, wondered it may work, but didn't.
// .fade {
// display: block !important;
// opacity: 1 !important;
// }
<button type="button" id="fadebtn">Fade</button>
<div id="box"></div>
I wrote this due to the title of the question: "Fade in ... pure javascript ... simple way."
tl;dr https://jsfiddle.net/nqfud4j0/
The following solution is a basic example of how you can use only Javascript to fade in/out to a desired value. You could also use this with other values/properties, but it also serves as an example for basic tweening.
It's intentionally using setInterval rather than requestAnimationFrame to demonstrate the example's use of time + controlled framerate rather than a delta or 'fast as possible.' A good solution would abstract this logic into a tweening library that combines both RAF + intervals to manage latency between frames.
function fadeTo(element, toValue = 0, duration = 200) {
// Store our element's current opacity (or default to 1 if null)
const fromValue = parseFloat(element.style.opacity) || 1;
// Mark the start time (in ms). We use this to calculate a ratio
// over time that applied to our supplied duration argument
const startTime = Date.now();
// Determines time (ms) between each frame. Sometimes you may not
// want a full 60 fps for performance reasons or aesthetic
const framerate = 1000 / 60; // 60fps
// Store reference to interval (number) so we can clear it later
let interval = setInterval(() => {
const currentTime = Date.now();
// This creates a normalized number between now vs when we
// started and how far into our desired duration it goes
const timeDiff = (currentTime - startTime) / duration;
// Interpolate our values using the ratio from above
const value = fromValue - (fromValue - toValue) * timeDiff;
// If our ratio is >= 1, then we're done.. so stop processing
if (timeDiff >= 1) {
clearInterval(interval);
interval = 0;
}
// Apply visual. Style attributes are strings.
element.style.opacity = value.toString();
}, framerate)
}
// Element reference
const element = document.querySelector('div');
// Fade in and out on click
element.addEventListener('click', e => {
// Animates our element from current opacity (1.0) to 0.25 for 1000ms
fadeTo(element, 0.25, 1000);
// Waits 1s, then animates our element's opacity to 1.0 for 500ms
setTimeout(() => {
fadeTo(element, 1.0, 500);
}, 1000);
});

Recursive setTimeout in eventListener function (drum sequencer)

I'm trying to fire a drum sequence on the click of a button using an event listener. Inside the event listener I iterate through the drum pattern using forEach and delay the playback of each (selected) iteration using setTimeout. The sequencer runs through one time and then stops. What I can't seem to figure out is how to structure the code block so that it continues to loop until the user clicks stop.
Here is what I have so far.
btn.addEventListener('click', () => {
cells.forEach((cell, i) => {
setTimeout(() => {
if (cell.classList.contains('selected')) {
cell.childNodes[1].play();
}
}, i * 500);
});
});
I've tried using a recursive function that was mentioned in another post by wrapping the forEach method inside another function and then recalling that function before the event listener function closes. I've also tried firing two separate setTimeout functions, the second of which recalls the function that is wrapped around the forEach method.
Any help would be greatly appreciated.
Thanks!
this might not be exactly what you are after, but I would strongly recommend moving this code into an ES6 class.
The main issue with what you have at the moment is that you don't know how many times the cells array is going to be looped over when the click event is fired. Your code essentially sets all of the timeouts at the start (think about setting 4 separate timers, one starting from 500 another from 1000, 1500, 2000). If you wanted this looping to be able to occur indefinitely, you would need an indefinite amount, or perhaps an individual timer for each beat that resets when it reaches the end.
Instead, I think a better solution is to store a variable that indicates the current position in the sequencer and sets a timeout that increments this variable after x amount of time, and resets back to the start once it reaches the end. This way you have only one timer that is counting down the time till the next beat. Once it reaches 0 it plays the next note and restarts.
class Sequencer {
// playButton = btn;
cells = [1,2,3,4]; // you would want to set this to your cells array
currentBeatIndex = 0;
isPlaying = false;
constructor(msPerBeat) {
this.msPerBeat = msPerBeat;
// this.playButton.addEventListener('click', () => this.toggleStartStop())
}
toggleStartStop() {
if (this.isPlaying) {
this.stop();
} else {
this.start();
}
}
stop() {
this.isPlaying = false;
// Could also set this.currentBeatIndex = 0 here if you wanted to return to the first beat when stopped.
}
start() {
this.isPlaying = true;
this.playCurrentNoteAndSetTimeout();
}
playCurrentNoteAndSetTimeout() {
if (this.isPlaying) {
console.log(this.cells[this.currentBeatIndex]); // use your .play() method here
setTimeout(() => {
this.toNextBeat();
this.playCurrentNoteAndSetTimeout(); // this is where your reccursion happens.
}, this.msPerBeat)
}
}
toNextBeat() {
this.currentBeatIndex = ++this.currentBeatIndex % this.cells.length; // mod operator here allows us to loop back to beginning of array when we reach the end.
}
}
const sequencer = new Sequencer(500) // 120 bpm
Try pasting this into your console and running sequencer.start() and then sequencer.stop()
As a sidenote, I'd recommend watching this video about the event loop. I found it really helpful with understanding timeouts.
I think setInterval and clearInterval will probably do what you need.
Here I have a play method on my cells which simply pulses the circle over 500 ms, and I do something similar to your loop in runPattern. I call this function once to start, and then call setInterval(runPattern, /*time*/). Storing the result of that value in the variable interval, I can then later stop the interval by calling clearInterval(interval).
Note that stop does not stop the current sequence, only preventing the next one from beginning. While we could change that, it's much more invasive to the existing code.
const handler = (() => {
const [red, blue, green, yellow] = ['b1', 'b2', 'b3', 'b4'].map(
(id, _, __, node = document.getElementById(id)) => ({
play: () => {
node.classList.add('pulse')
setTimeout(() => node.classList.remove('pulse'), 500)
}
})
)
const cells = [red, green, red, blue, yellow, blue]
const runPattern = () => {
cells.forEach((cell, i) => {
setTimeout(() => {
cell.play()
}, i * 500);
});
}
let running = false;
let interval= null
return {
start: () => {
if (!running) {
running = true
runPattern()
interval= setInterval(runPattern, 500 * cells.length + 1000)
}
},
stop: () => {
running = false
clearInterval(interval)
}
}
})()
document.getElementById('start').addEventListener('click', handler.start)
document.getElementById('stop').addEventListener('click', handler.stop)
#balls {position: relative}
span {margin: 50px; position: absolute; top: 0; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; border: 1px solid #999}
.pulse {animation: pulse .5s;}
#b1 {background: #f00; left: 20px;}
#b2 {background: #00f; left: 90px;}
#b3 {background: #0f0; left: 160px}
#b4 {background: #ff0; left: 230px}
#keyframes pulse {
0% {width: 40px; height: 40px; margin: 50 50 50}
50% {width: 60px; height: 60px; margin: 30 40 60}
100% {width: 40px; height 60px; margin: 50 50 50}
}
<button id="start">Start</button>
<button id="stop">Stop</button>
<div id ="balls">
<span id="b1"></span>
<span id="b2"></span>
<span id="b3"></span>
<span id="b4"></span>
</div>

Why does setting CSS property using Promise.then not actually happen at the then block?

Please try and run the following snippet, then click on the box.
const box = document.querySelector('.box')
box.addEventListener('click', e => {
if (!box.style.transform) {
box.style.transform = 'translateX(100px)'
new Promise(resolve => {
setTimeout(() => {
box.style.transition = 'none'
box.style.transform = ''
resolve('Transition complete')
}, 2000)
}).then(() => {
box.style.transition = ''
})
}
})
.box {
width: 100px;
height: 100px;
border-radius: 5px;
background-color: #121212;
transition: all 2s ease;
}
<div class = "box"></div>
What I expect to happen:
Click happens
Box starts translating horizontally by 100px (this action takes two seconds)
On click, a new Promise is also created. Inside said Promise, a setTimeout function is set to 2 seconds
After the action is completed (two seconds have elapsed), setTimeout runs its callback function and set transition to none. After doing that, setTimeout also reverts transform to its original value, thus rendering the box to appear at the original location.
The box appears at the original location with no transition effect problem here
After all of those finish, set the transition value of the box back to its original value
However, as can be seen, the transition value does not seem to be none when running. I know that there are other methods to achieve the above, e.g. using keyframe and transitionend, but why does this happen? I explicitly set the transition back to its original value only after the setTimeout finishes its callback, thus resolving the Promise.
EDIT
As per request, here's a gif of the code displaying the problematic behaviour:
The event loop batches style changes. If you change the style of an element on one line, the browser doesn't show that change immediately; it'll wait until the next animation frame. This is why, for example
elm.style.width = '10px';
elm.style.width = '100px';
doesn't result in flickering; the browser only cares about the style values set after all Javascript has completed.
Rendering occurs after all Javascript has completed, including microtasks. The .then of a Promise occurs in a microtask (which will effectively run as soon as all other Javascript has finished, but before anything else - such as rendering - has had a chance to run).
What you're doing is you're setting the transition property to '' in the microtask, before the browser has started rendering the change caused by style.transform = ''.
If you reset the transition to the empty string after a requestAnimationFrame (which will run just before the next repaint), and then after a setTimeout (which will run just after the next repaint), it'll work as expected:
const box = document.querySelector('.box')
box.addEventListener('click', e => {
if (!box.style.transform) {
box.style.transform = 'translateX(100px)'
setTimeout(() => {
box.style.transition = 'none'
box.style.transform = ''
// resolve('Transition complete')
requestAnimationFrame(() => {
setTimeout(() => {
box.style.transition = ''
});
});
}, 2000)
}
})
.box {
width: 100px;
height: 100px;
border-radius: 5px;
background-color: #121212;
transition: all 2s ease;
}
<div class="box"></div>
You are facing a variation of the transition doesn't work if element start hidden problem, but directly on the transition property.
You can refer to this answer to understand how the CSSOM and the DOM are linked for the "redraw" process.
Basically, browsers will generally wait until the next painting frame to recalculate all the new box positions and thus to apply CSS rules to the CSSOM.
So in your Promise handler, when you reset the transition to "", the transform: "" has still not been calculated yet. When it will get calculated, the transition will already have been reset to "" and the CSSOM will trigger the transition for the transform update.
However, we can force the browser to trigger a "reflow" and thus we can make it recalculate the position of your element, before we reset the transition to "".
const box = document.querySelector('.box')
box.addEventListener('click', e => {
if (!box.style.transform) {
box.style.transform = 'translateX(100px)'
new Promise(resolve => {
setTimeout(() => {
box.style.transition = 'none'
box.style.transform = ''
box.offsetWidth; // this triggers a reflow
resolve('Transition complete')
}, 2000)
}).then(() => {
box.style.transition = ''
})
}
})
.box {
width: 100px;
height: 100px;
border-radius: 5px;
background-color: #121212;
transition: all 2s ease;
}
<div class = "box"></div>
Which makes the use of the Promise quite unnecessary:
const box = document.querySelector('.box')
box.addEventListener('click', e => {
if (!box.style.transform) {
box.style.transform = 'translateX(100px)'
setTimeout(() => {
box.style.transition = 'none'
box.style.transform = ''
box.offsetWidth; // this triggers a reflow
// even synchronously
box.style.transition = ''
}, 2000)
}
})
.box {
width: 100px;
height: 100px;
border-radius: 5px;
background-color: #121212;
transition: all 2s ease;
}
<div class = "box"></div>
And for an explanation on micro-tasks, like Promise.resolve() or MutationEvents, or queueMicrotask(), you need to understand they'll get ran as soon as the current task is done, 7th step of the Event-loop processing model, before the rendering steps.
So in your case, it's very like if it were ran synchronously.
By the way, beware micro-tasks can be as blocking as a while loop:
// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );
I believe your issue is just that in your .then you are setting the transition to '', when you should be setting it to none as you did in the timer callback.
const box = document.querySelector('.box');
box.addEventListener('click', e => {
if (!box.style.transform) {
box.style.transform = 'translateX(100px)';
new Promise(resolve => {
setTimeout(() => {
box.style.transition = 'none';
box.style.transform = '';
resolve('Transition complete');
}, 2000)
}).then(() => {
box.style.transition = 'none'; // <<----
})
}
})
.box {
width: 100px;
height: 100px;
border-radius: 5px;
background-color: #121212;
transition: all 2s ease;
}
<div class = "box"></div>
I appreciate this isn't quite what you're looking for, but - out of curiosity and for the sake of completeness - I wanted to see if I could write a CSS-only approach to this effect.
Almost... but it turns out I still had to include a single line of javascript.
Working Example:
document.querySelector('.box').addEventListener('animationend', (e) => e.target.blur());
.box {
width: 100px;
height: 100px;
border-radius: 5px;
background-color: #121212;
cursor: pointer;
}
.box:focus {
animation: boxAnimation 2s ease;
}
#keyframes boxAnimation {
100% {transform: translateX(100px);}
}
<div class="box" tabindex="0"></div>

Trigger CSS transition on appended element

As this question observes, immediate CSS transitions on newly-appended elements are somehow ignored - the end state of the transition is rendered immediately.
For example, given this CSS (prefixes omitted here):
.box {
opacity: 0;
transition: all 2s;
background-color: red;
height: 100px;
width: 100px;
}
.box.in { opacity: 1; }
The opacity of this element will be set immediately to 1:
// Does not animate
var $a = $('<div>')
.addClass('box a')
.appendTo('#wrapper');
$a.addClass('in');
I have seen several ways of triggering the transition to get the expected behaviour:
// Does animate
var $b = $('<div>')
.addClass('box b')
.appendTo('#wrapper');
setTimeout(function() {
$('.b').addClass('in');
},0);
// Does animate
var $c = $('<div>')
.addClass('box c')
.appendTo('#wrapper');
$c[0]. offsetWidth = $c[0].offsetWidth
$c.addClass('in');
// Does animate
var $d = $('<div>')
.addClass('box d')
.appendTo('#wrapper');
$d.focus().addClass('in');
The same methods apply to vanilla JS DOM manipulation - this is not jQuery-specific behaviour.
Edit - I am using Chrome 35.
JSFiddle (includes vanilla JS example).
Why are immediate CSS animations on appended elements ignored?
How and why do these methods work?
Are there other ways of doing it
Which, if any, is the preferred solution?
The cause of not animating the newly added element is batching reflows by browsers.
When element is added, reflow is needed. The same applies to adding the class. However when you do both in single javascript round, browser takes its chance to optimize out the first one. In that case, there is only single (initial and final at the same time) style value, so no transition is going to happen.
The setTimeout trick works, because it delays the class addition to another javascript round, so there are two values present to the rendering engine, that needs to be calculated, as there is point in time, when the first one is presented to the user.
There is another exception of the batching rule. Browser need to calculate the immediate value, if you are trying to access it. One of these values is offsetWidth. When you are accessing it, the reflow is triggered. Another one is done separately during the actual display. Again, we have two different style values, so we can interpolate them in time.
This is really one of very few occasion, when this behaviour is desirable. Most of the time accessing the reflow-causing properties in between DOM modifications can cause serious slowdown.
The preferred solution may vary from person to person, but for me, the access of offsetWidth (or getComputedStyle()) is the best. There are cases, when setTimeout is fired without styles recalculation in between. This is rare case, mostly on loaded sites, but it happens. Then you won't get your animation. By accessing any calculated style, you are forcing the browser to actually calculate it.
Using jQuery try this (An Example Here.):
var $a = $('<div>')
.addClass('box a')
.appendTo('#wrapper');
$a.css('opacity'); // added
$a.addClass('in');
Using Vanilla javaScript try this:
var e = document.createElement('div');
e.className = 'box e';
document.getElementById('wrapper').appendChild(e);
window.getComputedStyle(e).opacity; // added
e.className += ' in';
Brief idea:
The getComputedStyle() flushes all pending style changes and
forces the layout engine to compute the element's current state, hence
.css() works similar way.
About css()from jQuery site:
The .css() method is a convenient way to get a style property from the
first matched element, especially in light of the different ways
browsers access most of those properties (the getComputedStyle()
method in standards-based browsers versus the currentStyle and
runtimeStyle properties in Internet Explorer) and the different terms
browsers use for certain properties.
You may use getComputedStyle()/css() instead of setTimeout. Also you may read this article for some details information and examples.
Please use the below code, use "focus()"
Jquery
var $a = $('<div>')
.addClass('box a')
.appendTo('#wrapper');
$a.focus(); // focus Added
$a.addClass('in');
Javascript
var e = document.createElement('div');
e.className = 'box e';
document.getElementById('wrapper').appendChild(e).focus(); // focus Added
e.className += ' in';
I prefer requestAnimationFrame + setTimeout (see this post).
const child = document.createElement("div");
child.style.backgroundColor = "blue";
child.style.width = "100px";
child.style.height = "100px";
child.style.transition = "1s";
parent.appendChild(child);
requestAnimationFrame(() =>
setTimeout(() => {
child.style.width = "200px";
})
);
Try it here.
#Frizi's solution works, but at times I've found that getComputedStyle has not worked when I change certain properties on an element. If that doesn't work, you can try getBoundingClientRect() as follows, which I've found to be bulletproof:
Let's assume we have an element el, on which we want to transition opacity, but el is display:none; opacity: 0:
el.style.display = 'block';
el.style.transition = 'opacity .5s linear';
// reflow
el.getBoundingClientRect();
// it transitions!
el.style.opacity = 1;
Anything fundamentally wrong with using keyframes for "animate on create"?
(if you strictly don't want those animations on the initial nodes, add another class .initial inhibitin animation)
function addNode() {
var node = document.createElement("div");
var textnode = document.createTextNode("Hello");
node.appendChild(textnode);
document.getElementById("here").appendChild(node);
}
setTimeout( addNode, 500);
setTimeout( addNode, 1000);
body, html { background: #444; display: flex; min-height: 100vh; align-items: center; justify-content: center; }
button { font-size: 4em; border-radius: 20px; margin-left: 60px;}
div {
width: 200px; height: 100px; border: 12px solid white; border-radius: 20px; margin: 10px;
background: gray;
animation: bouncy .5s linear forwards;
}
/* suppres for initial elements */
div.initial {
animation: none;
}
#keyframes bouncy {
0% { transform: scale(.1); opacity: 0 }
80% { transform: scale(1.15); opacity: 1 }
90% { transform: scale(.9); }
100% { transform: scale(1); }
}
<section id="here">
<div class="target initial"></div>
</section>
Rather than trying to force an immediate repaint or style calculation, I tried using requestAnimationFrame() to allow the browser to paint on its next available frame.
In Chrome + Firefox, the browser optimizes rendering too much so this still doesn't help (works in Safari).
I settled on manually forcing a delay with setTimeout() then using requestAnimationFrame() to responsibly let the browser paint. If the append hasn't painted before the timeout ends the animation might be ignored, but it seems to work reliably.
setTimeout(function () {
requestAnimationFrame(function () {
// trigger the animation
});
}, 20);
I chose 20ms because it's larger than 1 frame at 60fps (16.7ms) and some browsers won't register timeouts <5ms.
Fingers crossed that should force the animation start into the next frame and then start it responsibly when the browser is ready to paint again.
setTimeout() works only due to race conditions, requestAnimationFrame() should be used instead. But the offsetWidth trick works the best out of all options.
Here is an example situation. We have a series of boxes that each need to be animated downward in sequence. To get everything to work we need to get an animation frame twice per element, here I put once before the animation and once after, but it also seems to work if you just put them one after another.
Using requestAnimationFrame twice works:
Works regardless of how exactly the 2 getFrame()s and single set-class-name step are ordered.
const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));
async function run() {
for (let i = 0; i < 100; i++) {
const box = document.createElement('div');
document.body.appendChild(box);
// BEFORE
await getFrame();
//await delay(1);
box.className = 'move';
// AFTER
await getFrame();
//await delay(1);
}
}
run();
div {
display: inline-block;
background-color: red;
width: 20px;
height: 20px;
transition: transform 1s;
}
.move {
transform: translate(0px, 100px);
}
Using setTimeout twice fails:
Since this is race condition-based, exact results will vary a lot depending on your browser and computer. Increasing the setTimeout delay helps the animation win the race more often, but guarantees nothing.
With Firefox on my Surfacebook 1, and with a delay of 2ms / el, I see about 50% of the boxes failing. With a delay of 20ms / el I see about 10% of the boxes failing.
const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));
async function run() {
for (let i = 0; i < 100; i++) {
const box = document.createElement('div');
document.body.appendChild(box);
// BEFORE
//await getFrame();
await delay(1);
box.className = 'move';
// AFTER
//await getFrame();
await delay(1);
}
}
run();
div {
display: inline-block;
background-color: red;
width: 20px;
height: 20px;
transition: transform 1s;
}
.move {
transform: translate(0px, 100px);
}
Using requestAnimationFrame once and setTimeout usually works:
This is Brendan's solution (setTimeout first) or pomber's solution (requestAnimationFrame first).
# works:
getFrame()
delay(0)
ANIMATE
# works:
delay(0)
getFrame()
ANIMATE
# works:
delay(0)
ANIMATE
getFrame()
# fails:
getFrame()
ANIMATE
delay(0)
The once case where it doesn't work (for me) is when getting a frame, then animating, then delaying. I do not have an explanation why.
const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));
async function run() {
for (let i = 0; i < 100; i++) {
const box = document.createElement('div');
document.body.appendChild(box);
// BEFORE
await getFrame();
await delay(1);
box.className = 'move';
// AFTER
//await getFrame();
//await delay(1);
}
}
run();
div {
display: inline-block;
background-color: red;
width: 20px;
height: 20px;
transition: transform 1s;
}
.move {
transform: translate(0px, 100px);
}
Edit: the technique used in the original answer, below the horizontal rule, does not work 100% of the time, as noted in the comments by mindplay.dk.
Currently, if using requestAnimationFrame(), pomber's approach is probably the best, as can be seen in the article linked to in pomber's answer. The article has been updated since pomber answered, and it now mentions requestPostAnimationFrame(), available behind the Chrome flag --enable-experimental-web-platform-features now.
When requestPostAnimationFrame() reaches a stable state in all major browsers, this will presumably work reliably:
const div = document.createElement("div");
document.body.appendChild(div);
requestPostAnimationFrame(() => div.className = "fade");
div {
height: 100px;
width: 100px;
background-color: red;
}
.fade {
opacity: 0;
transition: opacity 2s;
}
For the time being, however, there is a polyfill called AfterFrame, which is also referenced in the aforementioned article. Example:
const div = document.createElement("div");
document.body.appendChild(div);
window.afterFrame(() => div.className = "fade");
div {
height: 100px;
width: 100px;
background-color: red;
}
.fade {
opacity: 0;
transition: opacity 2s;
}
<script src="https://unpkg.com/afterframe/dist/afterframe.umd.js"></script>
Original answer:
Unlike Brendan, I found that requestAnimationFrame() worked in Chrome 63, Firefox 57, IE11 and Edge.
var div = document.createElement("div");
document.body.appendChild(div);
requestAnimationFrame(function () {
div.className = "fade";
});
div {
height: 100px;
width: 100px;
background-color: red;
}
.fade {
opacity: 0;
transition: opacity 2s;
}

Categories

Resources