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 saidPromise
, asetTimeout
function is set to 2 seconds - After the action is completed (two seconds have elapsed),
setTimeout
runs its callback function and settransition
to none. After doing that,setTimeout
also revertstransform
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>