Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with:

Understanding CSS Transitions And Class Timing (Revisited)

By Ben Nadel on

Yesterday, I looked at CSS transitions and examined the timing in which the transitions would take effect. And, again, I don't mean the duration of the transition; or knowing when the transition ended. I mean, when does the browser actually initiate a transition in relationship to your mutation of an element's CSS properties. In the comments to that post, a number of people were confused by my confusion over CSS transitions. And, since I'm very new to transitions, it's highly possible that my confusion is just a symptom of my own ignorance. But, in any case, I thought I would try to come up with another, perhaps more concrete example to demonstrate why CSS transition timing has tripped me up in the past.


 
 
 

 
  
 
 
 

In the following code, I have a box whose default behavior is to use transitions. And, I have a button that will move the box to a new X/Y coordinate. Sometimes, however, I don't want the box to transition from one place to another - I want it to immediately jump to the desired location. In such a case, I need to temporarily override the default transition behavior as I am modifying the element's offsets. And, it is in that moment where understanding the "transition timing" seems to be critical:

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • CSS Transitions And Class Timing (Revisited)
  • </title>
  •  
  • <style type="text/css">
  •  
  • div.box {
  • background-color: #FAFAFA ;
  • border: 1px solid #CCCCCC ;
  • height: 100px ;
  • left: 20px ;
  • line-height: 100px ;
  • position: fixed ;
  • text-align: center ;
  • top: 120px ;
  • width: 100px ;
  •  
  • /* Start out with transition behavior. */
  • transition: all 1s ease ;
  • -webkit-transition: all 1s ease ;
  • }
  •  
  • div.jumper {
  • transition: none ;
  • -webkit-transition: none ;
  • }
  •  
  • </style>
  • </head>
  • <body>
  •  
  • <h1>
  • CSS Transitions And Class Timing (Revisited)
  • </h1>
  •  
  • <p>
  • <a href="#" class="move">Move To</a>
  • &mdash;
  • <a href="#" class="jump">Jump To</a>
  • &mdash;
  • <a href="#" class="jump-redraw">Jump To Redraw</a>
  • </p>
  •  
  • <div class="box">
  • I Am Box
  • </div>
  •  
  •  
  •  
  • <!-- Load jQuery from the CDN. -->
  • <script
  • type="text/javascript"
  • src="//code.jquery.com/jquery-1.9.1.min.js">
  • </script>
  • <script type="text/javascript">
  •  
  •  
  • // I return random X/Y coordinates for the demo.
  • function randCoordinates() {
  •  
  • return({
  • x: randRange( 20, 500 ),
  • y: randRange( 120, 300 )
  • });
  •  
  • }
  •  
  •  
  • // I return a pseudo-random number within the given range.
  • function randRange( min, max ) {
  •  
  • var range = ( max - min + 1 );
  •  
  • return(
  • min + Math.floor( Math.random() * range )
  • );
  •  
  • }
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • var box = $( "div.box" );
  •  
  •  
  • /// I simply change the X/Y coordinates, allowing the default
  • // transition behavior to take effect.
  • $( "a.move" ).click(
  • function() {
  •  
  • var coordinates = randCoordinates();
  •  
  • box.css({
  • left: ( coordinates.x + "px" ),
  • top: ( coordinates.y + "px" )
  • });
  •  
  • }
  • );
  •  
  •  
  • // I try to JUMP to the new X/Y coordinates without the default
  • // transition behavior by overriding the "transition" CSS
  • // property during the CSS change..... or do I ?!?!?!?!
  • $( "a.jump" ).click(
  • function() {
  •  
  • var coordinates = randCoordinates();
  •  
  • box.addClass( "jumper" );
  •  
  • box.css({
  • left: ( coordinates.x + "px" ),
  • top: ( coordinates.y + "px" )
  • });
  •  
  • box.removeClass( "jumper" );
  •  
  • }
  • );
  •  
  •  
  • // I try to JUMP to the new X/Y coordinate without the default
  • // transition behavior by overriding the "transition" CSS
  • // property during the CSS change. And, I force a repaint of
  • // the UI in order to update the offsets before removing the
  • // transition override.
  • $( "a.jump-redraw" ).click(
  • function() {
  •  
  • var coordinates = randCoordinates();
  •  
  • box.addClass( "jumper" );
  •  
  • box.css({
  • left: ( coordinates.x + "px" ),
  • top: ( coordinates.y + "px" )
  • });
  •  
  • // Forces a repaint in most browsers (apparently).
  • // This will cause the above CSS offsets to take
  • // effect before the transition override (ie. the
  • // "jumper" class) is removed.
  • var height = box[ 0 ].offsetHeight;
  •  
  • box.removeClass( "jumper" );
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, my first attempt to "jump" the box from one location to another does the following:

  1. Add transition-override class.
  2. Update CSS offsets.
  3. Remove transition-override class.

Unfortunately, since the browser is "bulking" these style modifications, the temporary override of the transition behavior doesn't get expressed. Instead, the browser simply looks at the outcome of the sum total of the updates amounting to nothing more than a change in offsets. This is why the first "Jump" link still causes the box to move by transition.

In the second "Jump" link, we do the same exact thing; however, after the offsets are changed, we force a repaint by querying the box's physical dimensions:

  1. Add transition-override class.
  2. Update CSS offsets.
  3. Query for physical dimensions of box.
  4. Remove transition-override class.

This forces the browser to apply the pending style changes, which at the time, has the transition-override in place. As such, the browser moves the box, without transition, to its new location before subsequently removing the transition override.

Again, I am very new to CSS transitions, so all of this may be old news to those of you that use transitions on a daily basis. But the tight relationship between the browser, bulking style changes, and CSS transitions has tripped me up a number of times in the past. Hopefully this post goes a bit further to point out why I find this stuff a little bit confusing.




Reader Comments

In my last post, @Ron pointed out that there was a behavioral difference in Firefox and Chrome as to what happens when a transition property is removed mid-transition.

In this demo, that doesn't matter since we're removing transitions, not adding them. As such, this demo works the same in all browsers that support transition that I checked (Firefox, Chrome, Safari).

Reply to this Comment

I had the same problem whilst developing http://www.sequencejs.com/. Sequence.js will add/remove classes as well as manipulate styles in quick succession and I found that the browser was doing this in a different order to what I expected.

My solution in the end was to wrap the latter class manipulation in a setTimeout(), with a 50ms delay. Although 10ms worked fine for Firefox, Chrome and Safari, Opera still had issues so I increased the delay until I reached 50ms and all browsers worked as expected.

I also looked at Mutation Events which let you know when a DOM element has changed but apparently they are terribly slow. Mutation Observers: https://developer.mozilla.org/en-US/docs/DOM/MutationObserver are meant to improve on Mutation Events but they're far from cross-browser so were a no-go for Sequence.js.

Have you found forcing a repaint to be reliable across browsers, including on mobile devices?

Reply to this Comment

@Ian,

I only recently found out about forcing repaints, based on this article by Alex MacCaw:

http://blog.alexmaccaw.com/css-transitions

In the article, he talks about some known issues with Android. But, other than that, that's as far as my experience goes.

I have seen some cross-browser differences between the setTimeout(). Specifically what you are talking about - one browser being fine with 10ms, the other browsers needing at least 50ms to show the same behavior.

Yo, your SequenceJS website is straight-up awesome!

Reply to this Comment

Hey, Ben. This is good stuff. I don't think you should feel "ignorant" or whatever, I think it's good to write what you discover, even if it seems simple.

In your previous post, if I'm understanding correctly, you could have resolved the whole issue of having the undesired jump by simply adding the transition CSS to the element itself to begin with, rather than to an added individual class called "transition". This is one of the subtleties of transitions, the fact that you can have "on" vs. "off" transitions.

However, in this 2nd post, you're doing that, so the problem in this unique case (which is somewhat opposite to your previous one) is indeed with the lack of delay and the synchronous nature of JavaScript.

And for the record, that trick on forcing repaints is very cool, I was not aware of that, even though I have seen that article by Alex and actually referred to it in a recent talk I did. And actually, you might like the talk, which covers transition stuff, which I screencasted here:

http://www.impressivewebs.com/jquery-toronto-slides-video/

Reply to this Comment

@Louis,

Thanks for the link, I'll check it out. I'm fairly new to all the transition stuff, so the more info I can get on it, the better! It seems fairly straightforward if the existence of the transition property is not dynamic. The part where it got murky for me was how it behaved if the transition was conditional. And, based on my previous blog post, it looks like that behavior is not consistent across browsers :(

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.