Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How long do I need to wait (setTimeout) to affect a class change when adding something to the DOM?

Here's the scenario... I add an element to the DOM that has an initial class to have 0 opacity and then add a class to trigger the opacity transition to 1 - a nice simple fade-in. Here's what the code might look like:

.test {
  opacity: 0;
  transition: opacity .5s;
}

.test.show {
  opacity: 1;
}
const el = document.createElement('div')
el.textContent = 'Hello world!'
el.className = 'test' // <div class="test">Hello world!</div>
document.body.appendChild(el)

Now to trigger the fade-in, I can simply add the show class to the element:

setTimeout(() => {
  el.classList.add('show')
}, 10)

I'm using a setTimeout because if I don't, the class will be added immediately and no fade-in will occur. It will just be initially visible on the screen with no transition. Because of this, I've historically used 10ms for the setTimeout and that's worked... until now. I've run into a scenario where I needed to up it to 20ms. This feels dirty. Does anyone know if there's a safe time to use? Am I missing something about how the DOM works here? I know I need to give the browser time to figure out layout and painting (hence the setTimeout), but how much time? I appreciate any insight!

like image 487
The Qodesmith Avatar asked Mar 04 '23 10:03

The Qodesmith


1 Answers

Note: It looks like you can avoid adding a second class to fade in the element and thus avoid this timing problem, see silencedogood's answer. If that works for you, it seems like a much better approach than the below.

If for some reason that won't work for you, read on... :-)


I don't think there's any reasonable, safe setTimeout value you could use.

Instead, I'd use requestAnimationFrame just after appending the element. In my experiments, you need to wait until the second animation frame, so crudely:

requestAnimationFrame(function() {
    requestAnimationFrame(function() {
        el.classList.add("show");
    });
});

Live Example:

document.getElementById("btn").addEventListener("click", function() {
    const el = document.createElement('div');
    el.textContent = 'Hello world!';
    el.className = 'test';
    document.body.appendChild(el);
    requestAnimationFrame(function() {
        requestAnimationFrame(function() {
            el.classList.add("show");
        });
    });
});
.test {
  opacity: 0;
  transition: opacity .5s;
}

.test.show {
  opacity: 1;
}
<input type="button" id="btn" value="Click me">

My logic for this is that when you've seen the first animation frame, you know that the DOM has been rendered with your element in it. So adding the class on the second animation frame should logically be after it's already been rendered without it, and so trigger the transition.

If I do it in the first animation frame, it doesn't work reliably for me on Firefox:

document.getElementById("btn").addEventListener("click", function() {
    const el = document.createElement('div');
    el.textContent = 'Hello world!';
    el.className = 'test';
    document.body.appendChild(el);
    requestAnimationFrame(function() {
        //requestAnimationFrame(function() {
            el.classList.add("show");
        //});
    });
});
.test {
  opacity: 0;
  transition: opacity .5s;
}

.test.show {
  opacity: 1;
}
<input type="button" id="btn" value="Click me">

...and that kind of makes sense to me, since it would be adding the class before the first time the element is rendered. (It did work if I added the element on page load, but not when I introduced the button above.)

like image 99
T.J. Crowder Avatar answered May 01 '23 23:05

T.J. Crowder