1

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)

ShaneSauce
  • 209
  • 2
  • 9
  • https://github.com/nefe/You-Dont-Need-jQuery#8.3 – Mister Jojo Jul 24 '19 at 01:16
  • 1
    Since your element is hidden and then shown the opacity isn't taken into affect. Either use an animation or add a `requestAnimationFrame` to change the opacity from 0 to 1 right after you set the element to display: block. – Kyle Jul 24 '19 at 01:37
  • @MisterJojo thanks! That function for fadeOut is effectively the same as mine... That fadeIn function, however skirts the ideal solution by animating with `setInterval`. It seems that there has to be a way to do this and leverage the smoother, hardware accelerated css animation. – ShaneSauce Jul 24 '19 at 20:12
  • 1
    Thanks a ton @Kyle! I had to dig a bit into rAF, but this did the trick! – ShaneSauce Jul 26 '19 at 01:33

1 Answers1

1

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 :)

ShaneSauce
  • 209
  • 2
  • 9