100

As this question observes, immediate CSS transitions on newly-appended elements are somehow ignored - the end state of the transition is rendered immediately.

For example, given this CSS (prefixes omitted here):

.box { 
  opacity: 0;
  transition: all 2s;
  background-color: red;
  height: 100px;
  width: 100px;
}

.box.in { opacity: 1; }

The opacity of this element will be set immediately to 1:

// Does not animate
var $a = $('<div>')
    .addClass('box a')
    .appendTo('#wrapper');
$a.addClass('in');

I have seen several ways of triggering the transition to get the expected behaviour:

// Does animate
var $b = $('<div>')
    .addClass('box b')
    .appendTo('#wrapper');

setTimeout(function() {
    $('.b').addClass('in');
},0);

// Does animate
var $c = $('<div>')
    .addClass('box c')
    .appendTo('#wrapper');

$c[0]. offsetWidth = $c[0].offsetWidth
$c.addClass('in');

// Does animate
var $d = $('<div>')
    .addClass('box d')
    .appendTo('#wrapper');
$d.focus().addClass('in');

The same methods apply to vanilla JS DOM manipulation - this is not jQuery-specific behaviour.

Edit - I am using Chrome 35.

JSFiddle (includes vanilla JS example).

  • Why are immediate CSS animations on appended elements ignored?
  • How and why do these methods work?
  • Are there other ways of doing it
  • Which, if any, is the preferred solution?
1
  • Good question, it's possible the animation is imminent due to the scripting engine optimizing your code.
    – Etheryte
    Commented Jun 10, 2014 at 18:34

9 Answers 9

166
+50

The cause of not animating the newly added element is batching reflows by browsers.

When element is added, reflow is needed. The same applies to adding the class. However when you do both in single javascript round, browser takes its chance to optimize out the first one. In that case, there is only single (initial and final at the same time) style value, so no transition is going to happen.

The setTimeout trick works, because it delays the class addition to another javascript round, so there are two values present to the rendering engine, that needs to be calculated, as there is point in time, when the first one is presented to the user.

There is another exception of the batching rule. Browser need to calculate the immediate value, if you are trying to access it. One of these values is offsetWidth. When you are accessing it, the reflow is triggered. Another one is done separately during the actual display. Again, we have two different style values, so we can interpolate them in time.

This is really one of very few occasion, when this behaviour is desirable. Most of the time accessing the reflow-causing properties in between DOM modifications can cause serious slowdown.

The preferred solution may vary from person to person, but for me, the access of offsetWidth (or getComputedStyle()) is the best. There are cases, when setTimeout is fired without styles recalculation in between. This is rare case, mostly on loaded sites, but it happens. Then you won't get your animation. By accessing any calculated style, you are forcing the browser to actually calculate it.

7
  • 2
    "The setTimeout trick works, because it delays the class addition to another javascript round" Note that on Firefox, the setTimeout duration matters. I don't know the details, but for me (Firefox 42, Linux), 3ms is the minimum for this to work: jsfiddle.net/ca3ee5y8/4 Commented Nov 14, 2015 at 15:51
  • @T.J.Crowder look's like firefox is trying to optimize stuff even further and if there was no paint and reflow before your timer callback got evaluated, you are out of luck. That's why it's good to know how to trigger it synchronously. You can do it in the beginning of the callback if you wish to.
    – Frizi
    Commented Nov 21, 2015 at 10:16
  • @Frizi excellent explanation. I'm curious, is there some official documentation that goes into this with some more depth? Something on MDN perhaps?
    – romellem
    Commented Dec 18, 2017 at 20:38
  • 4
    It looks like calling getComputedStyle on the element, even when assigning it to a variable, is optimized away by the JIT. (Only when I log it to the console does the reflow seem to occur. Accessing offsetWidth (even without use) seems to work though.
    – Kissaki
    Commented May 5, 2018 at 12:43
  • 1
    setTimeout(0) doesn't work. getComputedStyle() doesn't work. offsetWidth works. Commented Nov 20, 2019 at 19:01
36

Using jQuery try this (An Example Here.):

var $a = $('<div>')
.addClass('box a')
.appendTo('#wrapper');
$a.css('opacity'); // added
$a.addClass('in');

Using Vanilla javaScript try this:

var e = document.createElement('div');
e.className = 'box e';
document.getElementById('wrapper').appendChild(e);
window.getComputedStyle(e).opacity; // added
e.className += ' in';

Brief idea:

The getComputedStyle() flushes all pending style changes and forces the layout engine to compute the element's current state, hence .css() works similar way.

About css()from jQuery site:

The .css() method is a convenient way to get a style property from the first matched element, especially in light of the different ways browsers access most of those properties (the getComputedStyle() method in standards-based browsers versus the currentStyle and runtimeStyle properties in Internet Explorer) and the different terms browsers use for certain properties.

You may use getComputedStyle()/css() instead of setTimeout. Also you may read this article for some details information and examples.

2
  • 1
    Another great answer, +1 Commented Jun 19, 2014 at 12:52
  • I noticed that .css('opacity') was not flushing the style changes in IE11 with jquery version 2.2. Instead .offset() did the trick in IE11 Commented Dec 4, 2017 at 14:55
13

Please use the below code, use "focus()"

Jquery

var $a = $('<div>')
    .addClass('box a')
    .appendTo('#wrapper');
$a.focus(); // focus Added
$a.addClass('in');

Javascript

var e = document.createElement('div');
e.className = 'box e';
document.getElementById('wrapper').appendChild(e).focus(); // focus Added
e.className += ' in';
3
  • 2
    The question says this much. I want to understand why.
    – joews
    Commented Jun 19, 2014 at 10:44
  • 2
    This is the absolute best way. Just adding a ".focus()" after the appendChild is a nice and elegant solution. Commented Jun 4, 2019 at 10:50
  • @joews the problem is that this is copy pasted from the solution above - that's why it doesn't say anything
    – france1
    Commented Dec 28, 2022 at 9:19
9

I prefer requestAnimationFrame + setTimeout (see this post).

const child = document.createElement("div");
child.style.backgroundColor = "blue";
child.style.width = "100px";
child.style.height = "100px";
child.style.transition = "1s";

parent.appendChild(child);

requestAnimationFrame(() =>
  setTimeout(() => {
    child.style.width = "200px";
  })
);

Try it here.

2
  • Why do you like that better than simply adding ".focus()" at the end of your appendChild line? parent.appendChild(child).focus(); Commented Jun 4, 2019 at 10:52
  • I'm not sure why, but this was the only trick that worked consistently for me. Thanks! Commented Oct 3, 2022 at 13:27
7

@Frizi's solution works, but at times I've found that getComputedStyle has not worked when I change certain properties on an element. If that doesn't work, you can try getBoundingClientRect() as follows, which I've found to be bulletproof:

Let's assume we have an element el, on which we want to transition opacity, but el is display:none; opacity: 0:

el.style.display = 'block';
el.style.transition = 'opacity .5s linear';

// reflow
el.getBoundingClientRect();

// it transitions!
el.style.opacity = 1;
5

Anything fundamentally wrong with using keyframes for "animate on create"?

(if you strictly don't want those animations on the initial nodes, add another class .initial inhibitin animation)

function addNode() {
  var node = document.createElement("div");
  var textnode = document.createTextNode("Hello");
  node.appendChild(textnode);

  document.getElementById("here").appendChild(node);  
}

setTimeout( addNode, 500);
setTimeout( addNode, 1000);
body, html { background: #444; display: flex; min-height: 100vh; align-items: center; justify-content: center; }
button { font-size: 4em; border-radius: 20px; margin-left: 60px;}

div {
  width: 200px; height: 100px; border: 12px solid white; border-radius: 20px; margin: 10px;
  background: gray;
  animation: bouncy .5s linear forwards;
}

/* suppres for initial elements */
div.initial {
  animation: none;
}

@keyframes bouncy {
  0% { transform: scale(.1); opacity: 0 }  
  80% { transform: scale(1.15); opacity: 1 }  
  90% { transform: scale(.9); }  
  100% { transform: scale(1); }  
}
<section id="here">
  <div class="target initial"></div>
</section>

1
  • It has some flaws though. For instance, when you overwrite animation property in div:hover, then the initial animation will fire again after the element is "unhovered", which might be not desirable.
    – pumbo
    Commented Nov 11, 2021 at 6:44
3

Rather than trying to force an immediate repaint or style calculation, I tried using requestAnimationFrame() to allow the browser to paint on its next available frame.

In Chrome + Firefox, the browser optimizes rendering too much so this still doesn't help (works in Safari).

I settled on manually forcing a delay with setTimeout() then using requestAnimationFrame() to responsibly let the browser paint. If the append hasn't painted before the timeout ends the animation might be ignored, but it seems to work reliably.

setTimeout(function () {
    requestAnimationFrame(function () {
        // trigger the animation
    });
}, 20);

I chose 20ms because it's larger than 1 frame at 60fps (16.7ms) and some browsers won't register timeouts <5ms.

Fingers crossed that should force the animation start into the next frame and then start it responsibly when the browser is ready to paint again.

2
  • Yes. None of the other solutions worked for me. The only way i found is to setTimeout with minimum timeout of 15ms
    – Yashas
    Commented Apr 26, 2020 at 15:36
  • Would it be more reliable to do two nested requestAnimationFrame calls? 🤔 Commented Aug 14, 2020 at 14:29
3

setTimeout() works only due to race conditions, requestAnimationFrame() should be used instead. But the offsetWidth trick works the best out of all options.

Here is an example situation. We have a series of boxes that each need to be animated downward in sequence. To get everything to work we need to get an animation frame twice per element, here I put once before the animation and once after, but it also seems to work if you just put them one after another.

Using requestAnimationFrame twice works:

Works regardless of how exactly the 2 getFrame()s and single set-class-name step are ordered.

const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));

async function run() {
  for (let i = 0; i < 100; i++) {
    const box = document.createElement('div');
    document.body.appendChild(box);

    // BEFORE
    await getFrame();
    //await delay(1);

    box.className = 'move';
    
    // AFTER
    await getFrame();
    //await delay(1);
  }
}

run();
div {
  display: inline-block;
  background-color: red;
  width: 20px;
  height: 20px;
  
  transition: transform 1s;
}

.move {
  transform: translate(0px, 100px);
}

Using setTimeout twice fails:

Since this is race condition-based, exact results will vary a lot depending on your browser and computer. Increasing the setTimeout delay helps the animation win the race more often, but guarantees nothing.

With Firefox on my Surfacebook 1, and with a delay of 2ms / el, I see about 50% of the boxes failing. With a delay of 20ms / el I see about 10% of the boxes failing.

const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));

async function run() {
  for (let i = 0; i < 100; i++) {
    const box = document.createElement('div');
    document.body.appendChild(box);

    // BEFORE
    //await getFrame();
    await delay(1);

    box.className = 'move';
    
    // AFTER
    //await getFrame();
    await delay(1);
  }
}

run();
div {
  display: inline-block;
  background-color: red;
  width: 20px;
  height: 20px;
  
  transition: transform 1s;
}

.move {
  transform: translate(0px, 100px);
}

Using requestAnimationFrame once and setTimeout usually works:

This is Brendan's solution (setTimeout first) or pomber's solution (requestAnimationFrame first).

# works:
getFrame()
delay(0)
ANIMATE

# works:
delay(0)
getFrame()
ANIMATE

# works:
delay(0)
ANIMATE
getFrame()

# fails:
getFrame()
ANIMATE
delay(0)

The once case where it doesn't work (for me) is when getting a frame, then animating, then delaying. I do not have an explanation why.

const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));

async function run() {
  for (let i = 0; i < 100; i++) {
    const box = document.createElement('div');
    document.body.appendChild(box);

    // BEFORE
    await getFrame();
    await delay(1);

    box.className = 'move';
    
    // AFTER
    //await getFrame();
    //await delay(1);
  }
}

run();
div {
  display: inline-block;
  background-color: red;
  width: 20px;
  height: 20px;
  
  transition: transform 1s;
}

.move {
  transform: translate(0px, 100px);
}

1
  • setTimeout() works only due to race conditions: I think that's not true, setTimeout() works because it pushes the function you pass it into the callback queue, and then the even loop waits until the current call stack is emptied, and it then pushes the function into the call stack.
    – Arad
    Commented Feb 9, 2021 at 20:43
1

Edit: the technique used in the original answer, below the horizontal rule, does not work 100% of the time, as noted in the comments by mindplay.dk.

Currently, if using requestAnimationFrame(), pomber's approach is probably the best, as can be seen in the article linked to in pomber's answer. The article has been updated since pomber answered, and it now mentions requestPostAnimationFrame(), available behind the Chrome flag --enable-experimental-web-platform-features now.

When requestPostAnimationFrame() reaches a stable state in all major browsers, this will presumably work reliably:

const div = document.createElement("div");
document.body.appendChild(div);

requestPostAnimationFrame(() => div.className = "fade");
div {
  height: 100px;
  width: 100px;
  background-color: red;
}

.fade {
  opacity: 0;
  transition: opacity 2s;
}

For the time being, however, there is a polyfill called AfterFrame, which is also referenced in the aforementioned article. Example:

const div = document.createElement("div");
document.body.appendChild(div);

window.afterFrame(() => div.className = "fade");
div {
  height: 100px;
  width: 100px;
  background-color: red;
}

.fade {
  opacity: 0;
  transition: opacity 2s;
}
<script src="https://unpkg.com/afterframe/dist/afterframe.umd.js"></script>


Original answer:

Unlike Brendan, I found that requestAnimationFrame() worked in Chrome 63, Firefox 57, IE11 and Edge.

var div = document.createElement("div");
document.body.appendChild(div);

requestAnimationFrame(function () {
  div.className = "fade";
});
div {
  height: 100px;
  width: 100px;
  background-color: red;
}

.fade {
  opacity: 0;
  transition: opacity 2s;
}

4
  • 1
    Using requestAnimationFrame() works for me; I had the same issue from 4 years ago where I forced redraw using $element.hide().show().addClass("visible") to get my animation to work; this approach is much better!
    – Dan L
    Commented Dec 18, 2019 at 16:56
  • 1
    For me, this appears to work randomly... I'm inserting some HTML at page load, so there's a lot going in the browser at this time (loading/parsing more scripts, css, images, etc.) and every third or fourth refresh, the animation doesn't happen. I settled on two nested requestAnimationFrame calls for now, which does the job. Commented Aug 14, 2020 at 14:31
  • @mindplay.dk you're right. I managed to get the snippet to fail a few times (in Firefox, but not Chrome), so I've updated the answer. I'd be interested to know if either of the things I've added work reliably in your case (even if not in production)?
    – James T
    Commented Aug 15, 2020 at 19:48
  • @DanL the approach in the original answer doesn't work 100% of the time, so you may want to reevaluate the options, if you're still using this.
    – James T
    Commented Aug 15, 2020 at 19:58

Not the answer you're looking for? Browse other questions tagged or ask your own question.