4

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!

The Qodesmith
  • 3,205
  • 4
  • 32
  • 45
  • use [transitionend event](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/transitionend_event) – Endless Sep 13 '19 at 13:17
  • Why does 20ms feel dirty, but not 10ms :) ? You can accomplish this with pure css... – silencedogood Sep 13 '19 at 13:18
  • 1
    Instead of adding a class you could also use a `@keyframe` animation that’s applied to the element by default. – Ted Whitehead Sep 13 '19 at 13:19
  • 1
    `el.classname` should be `el.className`. It's best to use copy-and-paste rather than retyping your code in your question, as I assume it's not wrong in your actual code. – T.J. Crowder Sep 13 '19 at 13:19
  • 1
    @Endless that won’t work, the transition does not _start_ in the first place in this scenario. The opacity value changes from 0 to 1 while JS is still handling things, when control is passed back to rendering, that only sees the “final” value 1 and doesn’t notice that any _change_ has occurred, so no transition is happening in the first place. – misorude Sep 13 '19 at 13:32
  • 1
    @TedWhitehead It's not the specific amount of ms that feels dirty, but the fact that I had to increase with no objective way of knowing what is a solid amount. Just picking a value that works is what feels dirty. – The Qodesmith Sep 13 '19 at 13:53
  • @TheQodesmith - Doubting the use of arbitrary values arrived at via experimentation seems like a very good instinct indeed, at least to me. – T.J. Crowder Sep 13 '19 at 14:15
  • @TheQodesmith Yeah, that is odd. Can you provide more details about the scenario that led you to increase the delay to 20ms? Typically, any delay of 0 or more will work. See https://stackoverflow.com/questions/779379/why-is-settimeoutfn-0-sometimes-useful and https://stackoverflow.com/questions/33955650/what-is-settimeout-doing-when-set-to-0-milliseconds/33955673 for more details. – Ted Whitehead Sep 13 '19 at 14:36
  • @TedWhitehead I'm not trying to *just* pop something off the stack with setTimeout, rather, I'm playing around with starting with one set of CSS styles and having another set trigger a transition. For that to happen, the browser needs time to paint and reflow. I've historically used 10ms as my go to and it's seemed to work but recently I needed 20. I didn't like having to use arbitrary #'s. One thought is what if it works under my development conditions, in the browser I'm using, but doesn't for someone else possibly using a different browser? Hence this SO question. – The Qodesmith Sep 14 '19 at 21:57
  • @T.J.Crowder Thanks for the heads up. I didn't copy / paste since the actual code is much more involved. The code above get's the point across without puking too much code in the question. – The Qodesmith Sep 14 '19 at 22:03
  • @TheQodesmith - Do either of the two answers below solve the problem for you? – T.J. Crowder Sep 15 '19 at 08:08

2 Answers2

5

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.)

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • All of that said, having an element fade in when added is *incredibly* common, which makes me think we're working around something and there's a different way to achieve it that doesn't require the workaround... – T.J. Crowder Sep 13 '19 at 13:42
2

You could use a pure css solution, since you're concerned with javascript being potentially 'dirty' due to the arbitrary delay:

@keyframes fadein {
   from { opacity: 0; }
   to   { opacity: 1; }
}

.test {
   opacity: 0;
   animation: fadein 1s;
   animation-fill-mode: forwards;
}

Live Example:

document.getElementById("btn").addEventListener("click", function() {
    const el = document.createElement('div');
    el.textContent = 'Hello world!';
    el.className = 'test';
    document.body.appendChild(el);
});
@keyframes fadein {
   from { opacity: 0; }
   to   { opacity: 1; }
}

.test {
   opacity: 0;
   animation: fadein 1s;
   animation-fill-mode: forwards;
}
<input type="button" id="btn" value="Click me">
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
silencedogood
  • 3,209
  • 1
  • 11
  • 36