There are several StackOverflow posts about why changing an element's display from 'none' to 'box/flex/etc.' and making a CSS transition kick-in doesn't work.
The suggested solution, which makes sense, is to use requestAnimationFrame to make the CSS transition kick-in after the reflow.
One answer with a good explanation can be found here.
I've come across an interesting case where that doesn't work: when doing so in a 'transitionend' callback. Somehow, the requestAnimationFrame isn't enough in this case.
Quick setup:
A blue and red box. Blue box is faded in with this technique and it works. On blue box's transitionend, it is hidden and we use the same technique to show the red box. The red box shows instantly instead of fading in/having the opacity transition.
Also, added an onClick for the red box to redo the transition in the same manner - and it works.
let elem = document.getElementById("elem");
let elem2 = document.getElementById("elem2");
function addFadeElem2() {
elem2.classList.add("fade");
}
function switchElements() {
elem.classList.remove("fade") ;
elem2.style.display = "block";
window.requestAnimationFrame(addFadeElem2);
}
elem.style.display = "block";
window.requestAnimationFrame(() => {
elem.classList.add("fade");
});
function onTransitionEnd() {
switchElements();
//elem.offsetHeight; // Works with forced reflow
elem.removeEventListener("transitionend", onTransitionEnd);
}
elem.addEventListener("transitionend", onTransitionEnd);
function refade() {
elem2.style.display = "none";
elem2.classList.remove("fade");
window.requestAnimationFrame(() =>
{
elem2.style.display = "block";
window.requestAnimationFrame(addFadeElem2);
});
}
#elem {
background-color: blue;
}
#elem2 {
background-color: red;
}
.box {
display: none;
opacity : 0;
transition: opacity 1s;
transition-timing-function: ease-in;
width: 300px;
height: 250px;
}
.box.fade {
transition-timing-function: ease-out;
opacity: 1;
}
<div id="elem" class="box">aaaaa</div>
<div id="elem2" class="box" onclick="refade()">bbbbbbb</div>
If I force a reflow with offsetHeight, it works as expected. (of course if this was executed right after setting display: block, there is no longer a need for the requestAnimationFrame at all).
Likewise, "double-requestAnimationFrameing" also works.
Here are screenshots from devtools profiling:
Showing blue box / initial page load, transition works
Switch to showing red box, transition doesn't work
Noticeably when the transition doesn't work, the requestAnimationFrame callback occurs before there was a reflow with only display: box taken into account.
But why is that the case after a transitionend event and/or why is that not the case during page load or onClick? (Is the onClick a valid example of the technique working, or is it tainted by the display: block being set in a requestAnimationFrame callback).
I'm either missing or misunderstanding some fundementals.