When the dropdown opens, set it's display property to block, then set transition opacity from 0 to 1

When the dropdown closes, transition opacity from 1 to 0, then set it's display property to none

We can't build this with CSS transitions alone, because there's no way to change the display value before or after the transition happened. I don't like JavaScript animations for these things in JavaScript either, because it couples your JavaScript to CSS and vice-versa.

Making elements appear with enter

Let's zoom in on the enter function first. Before we dive into code, lets review what it needs to do:

Since the element isn't visible yet, remove display: none (we'll use a hidden class to control this)

Add the fade-enter class

Add the fade-enter-start class

Remove the fade-enter-start class. Since we just added it, we need to ensure the previous changes have been rendered before we remove it, otherwise it's a no-op.

Add the fade-enter-end class

When the transition is finished, remove the fade-enter-end and fade-enter classes

In short, we're gonna add and remove a bunch of classes.

functionenter(element,transition){element.classList.remove('hidden');element.classList.add(`${transition}-enter`);element.classList.add(`${transition}-enter-start`);// Wait until the above changes have been applied...
element.classList.remove(`${transition}-enter-start`);element.classList.add(`${transition}-enter-end`);// Wait until the transition is over...
element.classList.remove(`${transition}-enter-end`);element.classList.remove(`${transition}-enter`);}

First we'll solve the Wait until the above changes have been applied... problem.

To ensure the classList changes are applied before moving on to removing the *-start class, we can use the browser's requestAnimationFrame function. When you wrap a function in requestAnimationFrame, that function will execute after the browser did it's next repaint, in our case after the element's classList has been visibly modified.

functionenter(element,transition){element.classList.remove('hidden');element.classList.add(`${transition}-enter`);element.classList.add(`${transition}-enter-start`);requestAnimationFrame(()=>{element.classList.remove(`${transition}-enter-start`);element.classList.add(`${transition}-enter-end`);// Wait until the transition is over...
element.classList.remove(`${transition}-enter-end`);element.classList.remove(`${transition}-enter`);});}

Because of a Chrome bug, requestAnimationFrame sometimes incorrectly runs in the current frame (derp). The known workaround is wrapping it in anotherrequestAnimationFrame call.

functionenter(element,transition){element.classList.remove('hidden');element.classList.add(`${transition}-enter`);element.classList.add(`${transition}-enter-active`);requestAnimationFrame(()=>{requestAnimationFrame(()=>{element.classList.remove(`${transition}-enter-start`);element.classList.add(`${transition}-enter-end`);// Wait until the transition is over...
element.classList.remove(`${transition}-enter-end`);element.classList.remove(`${transition}-enter`);});});}

To keep our enter function clean, we'll extract the requestAnimationFrame logic to its own function that returns a promise, and turn enter into an async function.

asyncfunctionenter(element,transition){element.classList.remove('hidden');element.classList.add(`${transition}-enter`);element.classList.add(`${transition}-enter-active`);awaitnextFrame();element.classList.remove(`${transition}-enter`);// Wait until the transition is over...
element.classList.remove(`${transition}-enter-active`);}functionnextFrame(){returnnewPromise(resolve=>{requestAnimationFrame(()=>{requestAnimationFrame(resolve);});});}

Now we need to find a way to wait until the transition is over. To do that, we need to know how long the transition takes. We can determine that by parsing the transition duration with getComputedStyle, which allows us to read the applied CSS values from JavaScript.

Making elements disappear with leave

Now that we've learned how to make things appear, making them disappear shouldn't be too hard.

Once again, let's sketch out steps first:

Add the fade-leave class

Add the fade-leave-start class

Remove the fade-leave-start class. Since we just added it, we need to ensure the previous changes have been rendered before we remove it, otherwise it's a no-op.

Add the fade-leave-end class

When the transition is finished, remove the fade-leave-end and fade-leave classes

Finally, set the display property to none by adding a hidden class

In short, we're gonna add and remove a bunch of classes again.

functionleave(element,transition){element.classList.add(`${transition}-leave`);element.classList.add(`${transition}-leave-start`);// Wait until the above changes have been applied...
element.classList.remove(`${transition}-leave-start`);element.classList.add(`${transition}-leave-end`);// Wait until the transition is over...
element.classList.remove(`${transition}-leave-end`);element.classList.remove(`${transition}-leave`);element.classList.add('hidden');}

Nothing new here. We can fill in the gaps with the same utility functions as before.