Reputation: 221
I'm trying to implement an effect identical to jQuery's fadeIn()
function, where an element is displayed, and then it's opacity is animated from 0 to 1. I need to do it programmatically (WITHOUT jQuery), and I need the element to be able to fade out (display:none
) and then fade back in.
The ideal solution will use a CSS transition to leverage hardware acceleration - I can get the element to fadeOut with great success by listening to the transitionend
event. Fading back in, however is proving to be a challenge as the following bit of code is not working as intended:
fader.style.transition = 'opacity 1s';
const fadeIn = () => {
fader.style.display = 'block';
fader.style.opacity = 1;
};
When fadeIn()
is called the element simply snaps back in, instead of smoothly animating. I have a codePen that I've been tinkering with to illustrate the problem.
My theory is that the transition is unable to execute on an element that's not in the DOM, as I can get the animation to work by setting height:0
instead of display:none
. Perhaps there is a delay between when I set fader.style.display = 'block';
and when the DOM is actually being updated, during which I cannot transition?
On that idea: I also seem to be able to get the animation to work by delaying the opacity change with setTimeout(() => {fader.style.opacity = 1}, 20}
. This seems to create a sort of race condition however because as the timeout duration gets closer to 0 the animation works less and less dependably.
Please note that I do not want to toggle the visibility
attribute like the solutions to this question, as that does not effectively remove the element from the DOM.
Changing the height/width to 0 is a more viable option, but because the height and width of the element are not known, it will require the extra step of capturing those values before fading out so they can be re-applied when fading in. This seems flimsy if, say, a different part of the application tries to change those values (for example a media query, and the user rotates their device while the element is hidden)
Upvotes: 1
Views: 445
Reputation: 221
The following code should effectively replace jQuery's fadeOut()
and fadeIn()
functions (Much thanks to @Kyle for the clue!).
const fadeIn = (el, ms, callback) => {
ms = ms || 400;
const finishFadeIn = () => {
el.removeEventListener('transitionend', finishFadeIn);
callback && callback();
};
el.style.transition = 'opacity 0s';
el.style.display = '';
el.style.opacity = 0;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.addEventListener('transitionend', finishFadeIn);
el.style.transition = `opacity ${ms/1000}s`;
el.style.opacity = 1
});
});
};
const fadeOut = (el, ms, callback) => {
ms = ms || 400;
const finishFadeOut = () => {
el.style.display = 'none';
el.removeEventListener('transitionend', finishFadeOut);
callback && callback();
};
el.style.transition = 'opacity 0s';
el.style.opacity = 1;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.style.transition = `opacity ${ms/1000}s`;
el.addEventListener('transitionend', finishFadeOut);
el.style.opacity = 0;
});
});
};
This became super clear after digging into rAF (requestAnimationFrame
) and watching this video on the event loop. Right around 21:00
is where I had the aha moment about why rAF needs to be nested inside another rAF.
Here is the working example on Codepen.
Please comment if you discover any edge cases that aren't solved for :)
Upvotes: 1