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-requestAnimationFrame
ing" 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.