javascriptcsstransitionrace-condition

Immediately trigger transition after element created


I'm trying to create an action where a div is created and immediately "floats" upwards until it's off-screen.

To accomplish this I'm attempting to use a CSS transition, which will be completely driven by JavaScript (due to limitations in my use-case).

A problem occurs when I create an element, assign it it's transition style properties, and then immediately try to kick-off the transition by making a style change (top).

It looks like a timing issue is happening where the top style change is firing before the transition becomes available and thus simply moving my div off-screen immediately, rather than actually performing the transition.

Here's a simplified example:

var
    defaultHeight = 50,
    iCount = 0,
    aColors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];

function createBlock() {
    var testdiv = document.createElement('div');
    testdiv.className = 'testdiv';
    document.body.appendChild(testdiv);
    testdiv.style.left = '50%';
    testdiv.style.backgroundColor = aColors[iCount % aColors.length];
    testdiv.style.width = defaultHeight + 'px';
    testdiv.style.height = defaultHeight + 'px';
    testdiv.style.fontSize = '30px';
    testdiv.style.textAlign = 'center';
    testdiv.innerHTML = iCount;
    testdiv.style.top = '500px';
    testdiv.style.position = 'absolute';
    iCount++;
    return testdiv;
}

document.getElementById('go').onclick = function() {
    var testdiv = createBlock();

    testdiv.style.transition = "top 2.0s linear 0s";

    setTimeout(function() {
        testdiv.style.top = (defaultHeight*-2) + 'px';
    }, 0); // <- change this to a higher value to see the transition always occur
};

When the "go" button (see JSBin) is clicked rapidly the div only appears sporadically (presumably due to the timing issue described above).

If you increase the setTimeout's delay value you can see the transition almost always work.

Is there a way to deterministically kick off a transition immediately after creating an element (without having to resort to a delay)?


Solution

  • For a transition you need two distinct states aka. a change.

    Between two render-cycles styles are only overwritten. They are only applied when (re-)rendering the node.
    So, if your setTimeout() fires before the "old" styles have been applied, the stlyes are only overwritten, and your Nodes are rendered with the target-style.

    Afaik. most (desktop)-browsers strive for a 60fps framerate wich makes a 16.7ms interval. So setTimeout(fn,0) will most likely fire before that.

    You can either increase the timeout as you mentioned (I would recommend at least 50ms), or you can trigger/enforce the rendering of the node; for example by asking for it's size.

    To get the size of the Node, the browser first has to apply all styles to the Node, to know how they influence it.
    Also, context matters for css, so the Node has to be added somewhere to the DOM, for the browser to be able to get the finally computed styles.

    Long answer short:

    document.getElementById('go').onclick = function() {
        var testdiv = createBlock();
        testdiv.style.transition = "top 2.0s linear 0s";
    
        testdiv.scrollWidth;  //trigger rendering
    
        //JsBin brags that you don't do anything with the value.
        //and some minifyer may think it is irrelevant and remove it.
        //so, increment it (`++`); the value is readonly, you can do no harm by that.
        //but the mere expression, as shown, already triggers the rendering.
    
        //now set the target-styles for the transition
        testdiv.style.top = (defaultHeight*-2) + 'px';
    };