9

I am surprised by the fact that a CSS3 transition rule applied via jQuery after a jQuery-based CSS property change actually animates this property change. Please have a look at http://jsfiddle.net/zwatf/3/ :

Initially, a div is styled by two classes and has a certain height (200px) due to the default CSS properties of these two classes. The height is then modified with jQuery via removal of one class:

$('.container').removeClass('active');

This reduces the height from 200px to 15px.

After that, a transition rule is applied to the container via addition of a class:

$('.container').addClass('all-transition');

What is happening is that the reduction of the height becomes animated (on Firefox and Chrome, at least). In my world, this should not happen if the order of instructions has any meaning.

I guess this behavior can be very well explained. Why is that happening? How can I prevent it?

This is what I want to achieve:

  1. modify default style with jQuery (not animated by CSS3 transition!)
  2. apply transition rule with jQuery
  3. change a property with jQuery (animated by CSS3 transition)

(1) and (2) should happen as quickly as possible, so I do not like working with arbitrary delays.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Dr. Jan-Philip Gehrcke
  • 33,287
  • 14
  • 85
  • 130
  • This is really fricking weird and possibly a browser bug. I updated the fiddle to use removeClass and put a debugger break before the first function. http://jsfiddle.net/Danack/u9X4m/4/ If you step over the first function, then run the JS from there, everything works as you would expect. Without the debugger statement, there is the same weird animation effect that you're seeing. There is no code in removeClass to use any animation. I'm seeing this on Chrome btw. – Danack Mar 03 '13 at 14:07
  • Chrome 25.0.1364.152 on mac - shows animation incorrectly. Safari 5.1.7 (6534.57.2) on mac - shows animation incorrectly. – Danack Mar 03 '13 at 14:11
  • @abbood: I'm seeing an animation with Firefox 20 and Chrome 25 @ http://jsfiddle.net/zwatf/3/ – Dr. Jan-Philip Gehrcke Mar 03 '13 at 14:11
  • 1
    @abbood Similarly, please can you say which browser you're not seeing any animation in? – Danack Mar 03 '13 at 14:12
  • Opera does only show it when the classes are not applied on DOMready, but with a timeout: http://jsfiddle.net/zwatf/6/ – Bergi Mar 03 '13 at 14:14
  • Can someone who can run the webkit nightly builds http://nightly.webkit.org/ on a mac see if it happens in that? Unfortunately I can't due to an old OS. – Danack Mar 03 '13 at 14:25

3 Answers3

5

Why is that happening?

I guess because the DOM sees them applied together; immediate consecutive actions are usually cached instead of being applied one after the other.

How can I prevent it?

A (very small) timeout should do it: http://jsfiddle.net/zwatf/4/

(from the comments) Manually triggering a reflow also helps: http://jsfiddle.net/zwatf/9/

I do not like working with arbitrary delays.

Then I think the only other possible way is to remove the height from the animated css properties, i.e. you need to explicitly state which ones you wanted to animate: http://jsfiddle.net/zwatf/5/

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Bergi, do you know if jQuery is responsible for this "caching" behavior or if this is the browser itself? – Dr. Jan-Philip Gehrcke Mar 03 '13 at 14:15
  • "I guess because the DOM sees them applied together" That should be pronounced as "it's a browser bug". The DOM is meant to follow the rules it is given, not just implement them as it feels like. – Danack Mar 03 '13 at 14:20
  • Thanks Bergi for the linked question. Instead of a timeout, it seems to be safe to enforce a reflow according to "This effect is created when measurements are taken using properties like offsetWidth, or using methods like getComputedStyle. Even if the numbers are not used, simply using either of these while the browser is still caching changes, will be enough to trigger the hidden reflow." – Dr. Jan-Philip Gehrcke Mar 03 '13 at 14:21
  • @Danack: I'd see it as a feature of fast browsers, but you're free to file some bug reports for the gecko/webkit engines :-) If you do so, please link them here – Bergi Mar 03 '13 at 14:27
  • I'm pretty sure it's a bug. CSS should be deterministic, not dependent on external factors like CPU speed. I will file a bug report as soon as I can confirm it against a Webkit nightly build. – Danack Mar 03 '13 at 14:28
  • @Jan-PhilipGehrcke: Yeah, you could try that. I've never seen that hack, but I'd be curious to know whether it works :-) – Bergi Mar 03 '13 at 14:28
  • Bergi, it works in FF and Chrome. See http://jsfiddle.net/zwatf/8/ -- this actually exactly what I wanted to achieve. Can you confirm that it works in Opera (should see an animation from small to large)? – Dr. Jan-Philip Gehrcke Mar 03 '13 at 14:33
  • Yes, I can see the animation in Opera as well - yet I think a test would be where we should *not* see it: http://jsfiddle.net/zwatf/9/ (works in Opera) – Bergi Mar 03 '13 at 14:40
  • Bergi you added the outer `setTimeout(function(){ // for Opera` only to see the malicious/unexpected behavior in the first place, right? Without it, everything is fine anyway I understood. – Dr. Jan-Philip Gehrcke Mar 03 '13 at 14:47
  • Yes, it seems that Opera does not execute any transitions that are started during parsing of the document, i.e. dom manipulations in that phase have no effect. Changing the fiddle script from "in body" to "onDOMready" shows the unexpected behaviour as well. – Bergi Mar 03 '13 at 14:54
  • 1
    @Bergi Yep "It's a feature, not a bug" https://bugs.webkit.org/show_bug.cgi?id=111718 "CSS doesn't actually describe how and when styles are resolved, and most browsers batch style changes." Which seems insane to me, but hey, who am I to want APIs to behave predictably, rather than just however they feel like. – Danack Mar 08 '13 at 02:16
5

When running a script, the browser will usually defer DOM changes to the end to prevent unnecessary drawing. You can force a reflow by reading/writing certain properties:

var container = $('.container').removeClass('active');
var foo = container[0].offsetWidth;  //offsetWidth makes the browser recalculate
container.addClass('all-transition');

jsFiddle

Or you can use setTimeout with a short duration, which does basically the same thing by letting the browser reflow automatically, then adding the class.

Dennis
  • 32,200
  • 11
  • 64
  • 79
  • :-) Yeah, thanks, the related question that @Bergi mentioned also pointed me towards this technique. I will do some testings. So far, this "enforce-reflow-approach" seems to me to be quite reliable. – Dr. Jan-Philip Gehrcke Mar 03 '13 at 14:24
  • Yeah, it worked for me in both Aurora and Canary. IE10 doesn't seem to have the bug in the first place, but adding the line doesn't cause problems. – Dennis Mar 03 '13 at 14:36
  • It works, see http://jsfiddle.net/zwatf/8/ -- Bergi pointed this to me earlier than you, so you both have earned the green checkmark. I don't know what do do ;) – Dr. Jan-Philip Gehrcke Mar 03 '13 at 14:37
  • That's actually the issue, you both have earned it. I've given it to Dennis now, because the method that seems most reliable to me is the core of his answer. W.r.t. your answer, it's only part of the comment. – Dr. Jan-Philip Gehrcke Mar 03 '13 at 14:58
1

i think, it just happens too fast. if you do this:

$('.container').toggleClass('active',function() {
    $('.container').addClass('all-transition');
});

it will behave as you expected, right?

ok, fine, i tested it, but I didn't realize it didn't work as expected. Here an alternative to make it up:

$('.container').removeClass('active').promise().done(function(){
    $('.container').addClass('all-transition');

    $('.container').css('height','300'); //to see the easing
});
mindandmedia
  • 6,800
  • 1
  • 24
  • 33
  • 1
    No. [`.toggleClass`](http://api.jquery.com/toggleclass/) has no callback parameter – Bergi Mar 03 '13 at 13:56
  • Bergi is right. You did not try your code and did not look up the documentation. I think it's not because things are happening "too fast". Things happen uncontrolled. Coming from other languages, I would have expected that when `removeClass` returns that the action has already been already performed. Obviously, we're dealing with async execution. In this case, having a callback parameter would be nice. – Dr. Jan-Philip Gehrcke Mar 03 '13 at 14:09
  • I did not try the `promise`-based approach yet, but from the corresponding documentation it looks like this also is quite a reasonable approach -- **if** "once all actions of a certain type bound to the collection, queued or not, have ended" holds true also for the CSS change we need to wait for. – Dr. Jan-Philip Gehrcke Mar 03 '13 at 20:28