Inconsistent Transitions

Published 7 years, 3 months ago

Here’s an interesting little test case for transitions.  Obviously you’ll need to visit it in a browser that supports CSS transitions, and additionally also CSS 2D transforms.  (I’m not aware of a browser that supports the latter without supporting the former, but your rendering may vary.)

In Webkit and Gecko, hovering the first div causes the span to animate a 270 degree rotation over one second, but when you unhover the div the span immediately snaps back to its starting position.  In Opera 11, the span is instantly transformed when you hover and instantly restored to its starting position when you unhover.

In all three (Webkit, Gecko, and Opera), hovering the second div triggers a one-second 270-degree rotation of the span.  Unhovering causes the rotation animation to be reversed; that is, a one-second minus-270-degree rotation—or, if you mouseout from the div before the animation finishes, an rotation from that angle back to the starting position.  Either way, it’s completely consistent across browsers.

The difference is that in the first test case, both the transform and the transition are declared on hover.  Like this (edited for clarity):

div:hover span {
	transition: 1s transform;
	transform: rotate(270deg);
}

In the second test case, the transform and the transition are split up like so:

div span {
	transition: 1s transform;
}
div:hover span {
	transform: rotate(270deg);
}

It’s an interesting set of results.  Only the second case is consistently animated across the tested browsers, but the first case only animates one direction in Webkit and Gecko.  I’m not sure which, if any, of these results is more correct than the other.  It could well be that they’re all correct, even if not consistent; or that they’re all wrong, just in different ways.

At any rate, the takeaway here is that you probably don’t want to apply your transition properties to the hover state of the thing you’re transitioning, but to the unhovered state instead.  I say “probably” because maybe you like that it transitions on mouseover and instantly resets on mouseout.  I don’t know that I’d rely on that behavior, though.  It feels like the kind of thing that programmer action, or even spec changes, will take away.


  1. This actually looks like Gecko and Webkit got it right.

    When the transition is attached to the span it’s like saying ‘change your properties to whatever the effective values are in a 1sec timeframe’.
    So, because in the second case that transition property is always attached, the property changes are affected by the transition in both direction (hover on/off). But in the first case that transition is only attached when the div is :hover’d, so when the element isn’t hovered the transition is removed, and the other properties change instantly (no transition to follow).

    Off the top of my head it sounds like useful behavior as you can have different transitions for transitioning in/out of a state. Combining the examples:

    div span {
    transition: 3s transform; /* transition on blur slowly */
    }
    div:hover span {
    transition: 1s transform; /* transition on :hover fast */
    transform: rotate(270deg);
    }

  2. I actually find this to be very intuitive once I think about it. If the transition property is declared only for the :hover state then as soon as you mouseout the span no longer is in that state and so no longer has that property and so should not transition.

    Another interesting test-case could be to create another parent div surrounding the first with some padding (so it’s actually a larger surface). Then put the transform on the outer div:hover and the transition on the inner div:hover (both still applying to the span ultimately). As you mouseover you have to pass over the outer div before the inner div. I predict this will cause the transition to never be applied and it will snap into place both ways.

    An interesting Venn Diagram intersection could also be created that allowed every possibility of animate/don’t animate on mousein/mouseout depending on the path the user’s mouse takes across it.

  3. mouseleave
    div span {
    transition: 3s transform;
    }
    mouseenter
    div:hover span {
    transform: rotate(270deg);
    transition: 1s transform;
    }

  4. though the spec doesn’t say so explicitly, as far as i can tell, i always understood the transition property to be used to “prime” an element for a transition that’s going to happen later. in your first test this priming and the transform happen effectively at the same time, so Opera’s behaviour seems more logical to me. then again, you could say i’m biased ;)

  5. The timing of a transition does not work if you are also transforming. To see this, make the transition 5s and you will note that the transforms still snap. Just early days yet.

    I do recommend that you have one set of transitions of hover and another see of transitions on hover out since hovering out will also cause a snap.

  6. Patrick, it doesn’t make sense to me that the transition should be ignored if it’s defined on the hover state. For example:


    a[href]:hover {transform: rotate(90deg); color: red; transition: 1s all;}
    a[href]:focus {transform: skewX(-10deg); color: yellow; outline: 1px dotted green; transition: 2s all;}

    …reads to me as “On hover, do a 90-degree transform, color the link red, and take one second to do both. On focus, skew minus 10 degrees, color the link yellow, bring in a dotted green outline, and take two seconds to do all three.”

    Forcing the transition onto the default state means that all transitions are the same length. Allowing it on states, as Webkit and Gecko do, means you can vary lengths depending on the state. (Not to mention all the other transition-related properties.)

    Unless the specification says somewhere that transitions are ignored in such cases, Opera seems to be in the wrong. And if the specification does say that, I’d argue that it’s flawed and should be fixed.

  7. To everyone who feels the “snap back” behavior is correct, I’m not sure I agree. The section on Automatically reversing transitions seems to me to be written so that any animated transition should be smoothly reversed. It may interact with the rules on link states in some unexpected ways, but it seems to me the rotation should just be unwound.

    Of course, I’ve since found that both Opera and Webkit apparently ignore point 2 in that section (“Execute with the same duration T, but starting as if the transition had already begun…”) and run the reversal in T time, not T-TE time. Thus, if you start a 60 second rotation and abort it 10 seconds in, they take 60 seconds to rotate back to the starting point, not 10. Gecko does not suffer this failing.

    So yeah, CSS transitions. Kind of a mess.

  8. Interesting. Cedarholm’s CSS3 ABA book examples have the transition on the bare element, and I’m getting used to that now.

  9. The two examples behave exactly the same in my copy of Safari, Version 5.0.4 (6533.20.27) on Mac OS X 10.6.7 (i.e. the behavior you describe for the second span). However, when viewed in NetNewsWire (hence WebKit too but not Safari), the first span indeed snaps back to its position instantly on un-hovering.

  10. The relevant section of the spec on the testcase here is the section on starting of transitions. The point of the first sentence of that section is that the transition properties after the style change that triggers the transition are what matter. (I should probably add an example after that sentence…)

    This means that the snap-back behavior is correct. It also means that, if you want, you can use different timing functions for the “forwards” and “backwards” directions, but putting the timing function for the “forwards” direction on the :hover style and the “backwards” one on the style that’s always present.

  11. Thanks, David! If I understand you correctly, that makes Opera incorrect going forward (not backward) on the first test case, and all the browsers I mentioned correct on the second case. Right?

    Also, if a transition is only specified for the always-present style, then it would apply in both directions assuming no interruption, right? This is, given:

    a[href] {transition: 2s all;}
    a[href]:hover {transform: rotate(180deg);}

    …it would take two seconds to rotate on hover and the same two seconds to rotate back on un-hover, because the same transition would apply to both states. If I specified a different transition on the hover state, it would apply when un-hovering, not hovering. (And if the rotation on hover is interrupted, it should unspool in T-TE time, not take the full two seconds.) Right?

    Obviously I need to add cases to test transitions in both directions and on only un-hover to see how much consistency exists. Plus at least two more that test reversal time for interrupted animations.

    Fun fun fun!

  12. […] Inconsistent Transitions […]

  13. Eric, Opera’s behaviour will be updated to match the behavior specified in the spec soon.

  14. As a side note, webkit seems to apply the transformation to changes in text size on the second example, but not the first.

Leave a Comment

Management reserves the right to edit or remove any comment, especially when abusive or irrelevant to the topic at hand. HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <em> <i> <q cite=""> <s> <strong> <pre class=""> <kbd>


Comment Preview

If you're satisfied with what you've written, then go ahead...