CSS Transition fails on jQuery .load callback

The main solution is understanding setTimeout(fn, 0) and its usages.

This is not related to CSS animations or jQuery load method. This is a situation when you do multiple tasks into DOM. And delay time is not important at all, the main concept is using setTimeout. Useful answers and tutorials:

  • Why is setTimeout(fn, 0) sometimes useful? (stackoverflow answer)

  • Events and timing in-depth (real-life examples of setTimeout(fn, 0))

  • How JavaScript Timers Work (by jQuery Creator, John Resig)

function insertNoDelay() {
	$('<tr><td>No Delay</td></tr>')
		.appendTo('tbody')
		.addClass('green');
}

function insertWithDelay() {
	var $elem = $('<tr><td>With Delay</td></tr>')
		.appendTo('tbody');
	
	setTimeout(function () {
		$elem.addClass('green');
	}, 0);
}
tr { transition: background-color 1000ms linear; }
.green { background: green; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<table>
	<tbody>
		<tr><td>Hello World</td></tr>
	</tbody>
</table>


<button onclick="insertNoDelay()">No Delay</button>
<button onclick="insertWithDelay()">With Delay</button>

This is a common problem caused by browsers. Basically, when new element is inserted it is not inserted immediately. So when you add the class, it is still not in the DOM and how it will be rendered is calculated after the class is added. When the element is added to the DOM, it already has the green background, it never had a white background so there is no transition to do. There are workarounds to overcome this as suggested here and there. I suggest you use requestAnimationFrame like this:

function load_tbody(row_id) {
    $('tbody').load("load_tbody.php", function() {
        requestAnimationFrame(function() {
            $(row_id).addClass('green');
        });
    });
}

Edit:

Seems like the above solution doesn't work for all cases. I found an interesting hack here which triggers an event when an element is really parsed and added to the DOM. If you change the background color after the element is really added, the problem will not occur. Fiddle

Edit: If you want to try this solution (Won't work below IE9):

Include this CSS:

@keyframes nodeInserted {  
    from {  
        outline-color: #fff; 
    }
    to {  
        outline-color: #000;
    } 
}

@-moz-keyframes nodeInserted {  
    from {  
        outline-color: #fff; 
    }
    to {  
        outline-color: #000;
    }  
}

@-webkit-keyframes nodeInserted {  
    from {  
        outline-color: #fff; 
    }
    to {  
        outline-color: #000;
    }  
}

@-ms-keyframes nodeInserted {  
    from {  
        outline-color: #fff; 
    }
    to {  
        outline-color: #000;
    } 
}

@-o-keyframes nodeInserted {  
    from {  
        outline-color: #fff; 
    }
    to {  
        outline-color: #000;
    }  
} 

.nodeInsertedTarget {
    animation-duration: 0.01s;
    -o-animation-duration: 0.01s;
    -ms-animation-duration: 0.01s;
    -moz-animation-duration: 0.01s;
    -webkit-animation-duration: 0.01s;
    animation-name: nodeInserted;
    -o-animation-name: nodeInserted;
    -ms-animation-name: nodeInserted;        
    -moz-animation-name: nodeInserted;
    -webkit-animation-name: nodeInserted;
}

And use this javascript:

nodeInsertedEvent= function(event){
        event = event||window.event;
        if (event.animationName == 'nodeInserted'){
            var target = $(event.target);
            target.addClass("green");
        }
    }

document.addEventListener('animationstart', nodeInsertedEvent, false);
document.addEventListener('MSAnimationStart', nodeInsertedEvent, false);
document.addEventListener('webkitAnimationStart', nodeInsertedEvent, false);

function load_tbody(row_id) {
    $('tbody').load("load_tbody.php", function() {
        $(row_id).addClass('nodeInsertedTarget');
    });
}

Can be made into a generic solution or library. This is just a fast solution.


jQuery load is intended to drop everything that is requested into the page.

You can leverage the power of jQuery Deferred objects by using $.get instead.

Take a look at this plunk.

Code snippet from plunk

function load_tbody(row_id) {
  $.when($.get("load_tbody.html", function(response) {
    $('tbody').hide().html(response).fadeIn(100, function() {
      $(this).find(row_id).addClass('green');
    });
  }));
}

I am using the $.when which will run its callback as soon the $.get is resolved, meaning will fetch the HTML response. After the response is fetched, it is appended to the tbody, which is fadedIn (fadeIn method) and after it is shown, the .green class is added to the desired row.

Note that if you go and just append the html and then the class to the row_id, you won't be able to see the transition, because it is executed immediately. A little nice visual trick with the fadeIn can do the work.

Update

On newly added elements to the DOM, CSS3 transition is not going to be triggered. This mainly happens because of the internal browser engine that controls all animations. There are numerous articles with workarounds on the issue, as well as stackoverflow answers. Additional resources can be found there, which I believe can explain the topic much better than me.

This answer is about taking a step back and changing the piece of functionality that renders dynamic elements in DOM, without going to use setTimeout or requestAnimationFrame. This is just another way to achieve what you want to achieve, in clear and consistent way, as jQuery works across browsers. The fadeIn(100, ... is what is needed to catch up with the next available frame the browser is about to render. It could be much less, the value is just to satisfy visual aesthetics.

Another workaround is to not use transitions at all and use animation instead. But from my tests this fails in IE Edge, works well on Chrome, Firefox.

Please look at the following resources:

  • https://www.christianheilmann.com/2015/08/30/quicky-fading-in-a-newly-created-element-using-css/

Update 2

Take a look at the specification please, as interesting stuff lies there regarding CSS3 transitions.

...This processing of a set of simultaneous style changes is called a style change event. (Implementations typically have a style change event to correspond with their desired screen refresh rate, and when up-to-date computed style or layout information is needed for a script API that depends on it.)

Since this specification does not define when a style change event occurs, and thus what changes to computed values are considered simultaneous, authors should be aware that changing any of the transition properties a small amount of time after making a change that might transition can result in behavior that varies between implementations, since the changes might be considered simultaneous in some implementations but not others.

When a style change event occurs, implementations must start transitions based on the computed values that changed in that event. If an element is not in the document during that style change even or was not in the document during the previous style change event, then transitions are not started for that element in that style change event.


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

Trigger CSS transition on appended element

Explanation For the last part

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.

In a way .css() is jquery equivalent of javascript function getComputedStyle() which explains why adding the css property before adding class made everything work

Jquery .css() documentation

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


// Check it's not just jQuery
// does not animate
var e = document.createElement('div');
e.className = 'box e';
document.getElementById('wrapper').appendChild(e);
window.getComputedStyle(e).opacity;
e.className += ' in';
.box { 
  opacity: 0;
  -webkit-transition: all 2s;
     -moz-transition: all 2s;
          transition: all 2s;
    
  background-color: red;
  height: 100px;
  width: 100px;
  margin: 10px;
}

.box.in {
    opacity: 1;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<div id="wrapper"></div>

Here is the listed work arounds [SO Question]

css transitions on new elements [SO Question]