Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Dan Wilson
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Dan Wilson

Understanding CSS Transitions And Class Timing (Revisited)

By
Published in , Comments (6)

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.

Want to use code from this post? Check out the license.

Reader Comments

15,902 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).

1 Comments

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?

15,902 Comments

@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!

1 Comments

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/

15,902 Comments

@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 :(

1 Comments

Hi Ben, thank you so much for this post. I had a similar problem and your post made me rethink it from start to end and now I was able to fix it.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel