How to fix CSS transition from JavaScript

  • Today I struggled with a weird behavior of CSS transitions. In some cases they worked and in some not. So I investigated a bit and could find a solution and even kind of an explanation :D

    My use case was a overlay element whose height should be animated via CSS transitions when opening and closing based on the content it has. The content itself was absolute positioned for several reasons and I had to get the height of all children every time they change. In a previous version the code kind of looked like this:

    When the overlay opens, it get's the height of it's children and the children notifies the container when their height changes in meanwhile. This worked very well so far but I wanted to decouple the opening logic from the special overlay to have a more generic overlay with dynamic content. This generic overlay should not know anything about the children it renders but it's height. So I introduced a `setHeight(int)` function to the Overlay, which can be called from SpecialOverlay. To remember the height between close and open actions I introduced a variable for the contentHeight. When opening the overlay the first time, the contentHeight is calculated from the children. After this change my code looked like this:

    Now I had a problem

    For some reason the transition for the overlay opening only worked the first open action but then there was no transition at all for the opening. I checked out what exactly changed so far and couldn't find a critical issue. For some reasons one of my first try out's when struggling with browser behavior is to schedule the problematic part in a micro task with `setTimeout(fn, 0)` and see what happened -> it worked! :D

    I knew it was a problem with the browser task schedule, Googled around and found the reason.

    As you might know a browser runs your JavaScript code as tasks and microtasks in an event loop. If you don't know this you should read this article by Jake Archibald. What happened here is the browser is not applying the layout changes (styles, classes, etc) directly but schedules them to execute them in batches. This lead to have the height and the class with the transition applied at the same time which won't trigger the transition because effectively no change of the height happened.

    1. LayoutBatch {
    2. set height to 0;
    3. add class 'mod-open';
    4. set height to x;
    5. } = Styles to be applied {height: x; classList: 'mod-open';}

    How to solve this?

    Since I knew the layout engine causes my problem I had to find a way to tell the layout engine to first apply the `height: 0` and the new class `mod-open` and after this change the height to x. My preferred option of scheduling a micro task, I mentioned above already. Another option is to use render blocking properties such as clientHeight. Those properties lead the layout engine to evaluate the scheduled tasks because they might affect the height.

    Here is a JSFiddle example which illustrates the problem and the solutions.

    PS: I would be happy to learn other / better solutions :)