Advanced CSS Transitions & Animations Proposal

From Effects Task Force

(please see ACTA: Analysis of Missing Features for a description of what missing features are preventing efficient implementation of the ideas in this proposal in JavaScript)

The Problem

As currently defined by http://www.w3.org/TR/css3-transitions/, The transition property sets up a very simple animation (just a linear interpolation from the start value to the end value of a single property) that fires when an element moves between two states, where a state is a particular value of a given property.

As long as transition is specified for an element, any state-change in the specified properties will cancel any existing instance of the simple animation and fire a new one. It's the state change which is important for the triggering; transition itself should be specified on the element at all times if you want easily-predictable behavior. The affordances provided by the transitions specification are reasonably crude - they allow you to specify the properties over which transitions should occur, the length of time a transition should take, and a time function that determines the degree of interpolation of the transition across the specified length of time.

The animation property (as defined by http://www.w3.org/TR/css3-animations/), on the other hand, simply runs an animation constantly while an element is in a state, where a state is tied to the value of the animation property. It can appear that the animation is fired by the element entering some state, but that's an illusion; there is no state-changing behavior to speak of, so trying to use animation to do complex state transitions is a direct route to confusion and pain.

The Current Model

This section restates the current transition and animation specifications in more detail, so that the modifications suggested by this proposal are clear.

First, there are animations. These are defined in two ways:

  1. Keyframe animations. These can affect multiple properties at the same time, but the property values that they're animating are defined at creation time; you can't alter them on the fly based on current CSS values.
  2. Transitions. These are anonymous animations that simply animate a single property from a starting value to an ending value.

Then, there are ways to play the animations. Again, these are defined in two ways:

  1. Keyframe animations are played constantly as long as they're specified in the animation property for an element. (If an animation has a finite iteration count, it still plays forever, it just doesn't do anything once its iterations are done.)
  2. The transition property sets up transition animations on an element, which play whenever the property they're defined over changes its value.


Use-Cases

  1. The jQuery slideIn/slideOut animations. The mental model here is that these are fancy ways of transitioning to/from display:none. Right now, you have to do some complex and fragile hacking of classes and states to make this more-or-less work with the animation keyword; you can't simply say "any time this element goes to display:none, play the slideOut animation for it".
  2. Hovering/activating buttons and other complex widget-type things. There are four distinct interaction states here (normal, over, down, and out) with 7 distinct edges that users can traverse to switch between states (normal<=>over, over<=>down, down<=>out, out=>normal). In apps that pay a *lot* of attention to these things, many or all of these edges may have subtly distinct animations to maximally capture or avoid attention, as desired. For example, going from normal=>over may play over .2s, but going back from over=>normal may play in half the time. These animations may be complex (requiring keyframes), or trivial (just needing transitions), but you can't key any of them off a single property, as no single property uniquely describes the state in general.


The Proposal

We propose an extension to the T/A specs that allows authors to attach arbitrary keyframe-defined animations to arbitrary edges in a property-value graph, such that it's simple to have distinct complex animations run for every distinct pair of starting and ending values of a particular property. This proposal is in several, largely distinct, pieces.


Relative Keyframes

Right now, keyframes animations are absolute, in that you must fill in all values at the time of defining the keyframe. This is very limiting - it means you can't create a general "pulse" animation that makes an element get slightly wider and narrower, for example; instead, you have to create individual pulse animations for every box-size you want to animate, and you have to remember to change the animation if the size of the box is changed.

To fix this, we propose adding three new functions that are only valid inside keyframe rules - from(), to(), and prev(). For keyframe animations set on a transition (see below for details on this), the from() and to() functions as part of a property value resolve to the starting and ending values of that property, respectively. For keyframe animations played via the animation property, both functions resolve to the current value of the property. The prev() function is used to make dynamic stepping easier - if the property that prev() is used in was set in an earlier keyframe, prev() resolves to that value; otherwise, it's identical to from().

For example, we can use this to define an animation over opacity that is identical to the anonymous animations created by using transition over opacity:

@keyframes transition-opacity {
  0% { opacity: from(); }
  100% { opacity: to(); }
}

We can also use this in conjunction with calc() to make the general "pulse" animation mentioned previously:

@keyframes general-pulse {
  0% { width: from(); }
  25% { width: calc( from() + 5% ); }
  50% { width: from(); }
  75% { width: calc( from() - 5% ); }
  100% { width: from(); }
}

Setting Animations on Transition Edges

Right now, you can only set trivial animations on transition edges, and they must be the same animation on all edges of a particular property-value graph. Again, this is limiting, for reasons stated above in the button use-case.

To fix this, we propose two new @-rules that allow an author to specify an animation graph by associating keyframes with particular transition-edges for a set of elements. The syntax is as follows:

@transition-graph [selector] {
  over: [property name];
  @edge([property value], [property value]) { animation: [animation value]; }
 ...
}

This can be used, for example, to solve the slideIn/slideOut case:

@transition-graph .menu-item {
  over: display;
 @edge(block, none) { animation: slideOut .2s; }
  @edge(none, block) { animations: slideIn .2s; }
}

(Where slideIn and slideOut are keyframe animations that specify roughly what the current equivalent jQuery animations do manually, probably using the relative-value functions.)

This sets up two transition on all ".menu-item" elements: one for when the display value changes from block to none, and one for the reverse. Any other transition between display values will take effect immediately, as normal, and other elements won't have transitions at all (unless they're specified elsewhere).

(In case of conflict, @transition-graphs are cascaded the same way as a declaration block, according to their selector.)

Reversible edges

We propose that @edge blocks define a direction property which can be set to forward (default), reverse, or both. When direction: reverse is specified, the effect is that the @edge declaration runs from the to state to the from state, and the associated @keyframes animation is resolved to real values then reversed.

When direction: both is selected, the effect is identical to declaring two @edge blocks with the same animation statement, one specifying direction: forward and one specifying direction: reverse.

Generic rules

Instead of providing explicit from and to values in @edge, authors can specify generic rules by using '‘*'’ instead. For example, one could omit the block values from the previous example to make the slideIn/slideOut animations apply any time an element is changed to/from display:none, no matter what the source/dest display value is.

The transition property can now be viewed as a magical way of specifying an @transition-graph rule. Something like foo { transition: opacity .2s; } could instead be written as:

@transitionGraph foo {
  over: opacity;
 @edge(*, *) { animation: transition-opacity .2s; }
}

We also propose transition(), an animation-generating function that generates these trivial transitions for us, so that rather than having to define a transition-opacity animation using an @keyframes rule one could simply say animation: transition(opacity) .2s and get the same effect automatically.

Within a @transition-graph block, text order is important, so that the more recent of two @edge specifications with the same ‘'from'’ and '‘to'’ values wins. Specifying a wildcard value for from or to changes this behavior slightly - a rule with a wildcard for from or to loses to one with explicit values. This lets you set up default transitions that omit one or both of the endpoints, and then specify more specific transitions between particular states.

(An arbitrary choice needs to be made as to whether @edge(“*”, explicit) wins or loses to @edge(explicit, “*”))

(The interaction of the transition property and @transition-graphs specified on the same element should be pretty simple - transition is treated like it creates an @transition-graph like the above, but it always loses conflicts with explicit @transition-graph rules.)

Running an Animation on Arbitrary State Changes

Even with the previous addition, we still can't easily solve the Button use-case, because there isn't, in general, a single property which represents the four states with unique values, such that we can key some @edge rules off it. We can possibly hack this in by using some property that we very barely alter (like changing the text color from #000000 to #000001 to represent going from normal=>hover), but that's nasty.

To fix this, we propose the addition of a set of user-extensible properties to CSS that can take arbitrary values. These will have absolutely no effect on any type of rendering; they are to be used solely for keying transitions off. (They are roughly analogous to the user-extensible set of data-* attributes in HTML5, intended for storing private app-specific data in a well-known location.)

They would be used like so:

button { state-interaction: normal; }
button:hover { state-interaction: over; }
button:hover:active { state-interaction: down; }
button:active { state-interaction: out; }

@transitionGraph button {
  over: state-interaction;
  @edge(normal, over) { … }
   ...6 more transitions...
}

Any property starting with the state- prefix is guaranteed to be ignored for rendering purposes, but recognized as a valid property by the browser. The user can use anything after the prefix (subject to the normal property-name restrictions). state-* properties never inherit, and their computed and used values are always the same as their specified values.

(We’re not sure if we want/need any restrictions on their value; we can probably get by fine on saying that their value must be a single arbitrary keyword, but maybe it's useful to be able to specify more, like a keyword+int combo?)

Chaining animations together

Specifying transition animations as edges in a transition graph opens up two new types of animation effect to declarative specification:

  1. It’s now possible to accommodate animations that always finish animating, and in fact we propose that by default all animation edges do complete.
  2. It’s now possible to make use of the animation graph to chain animations together, so that sparse animation graphs can be defined that nevertheless behave appropriately under a wide range of conditions.

We propose that for animations controlled by @transition-graph rules, both the current visual state and the currently selected state are tracked independently. Take the following CSS:

.item {
  state-item: normal;
  …
}

.item:hover {
  state-item: hover;
  …
}

@transition-graph .item {
  over: state-item;
  @edge(normal, hover) { animation: … 2s; direction: both; }
}

Initially, a div selected by .item will be in both visual and selected state norma for the provided @transition-graph. If :hover is triggered, then the selected state will instantaneously change to hover. The visual state, on the other hand, will transition over 2 seconds from normal to hover. If during this 2 second transition a new selected state change occurs, then the selected state will instantaneously change but the visual state will remain at its current value. We propose giving an element of control over what happens to the visual state under these conditions to web developers via an interruptBehaviour property defined on @edge blocks.

interruptBehaviour modes

Edge completion

By default, interruptBehaviour has a value of complete. This indicates that once started, an animation edge will always finish. On completion there may be a disparity between the visual state and the selected state, in which case the shortest path from the visual state to the selected state will be computed, and the initial edge in this path will trigger a new animation edge.

If this edge does not lead directly to the selected state then it will be immediately interrupted and the interruption resolved according to the rules laid out in this section.

For example, taking the above CSS, if the .item is briefly hovered over, then a complete transition to the hover state will occur over 2 seconds (although the selected state will change back to normal after the hover, the current @edge has an interruptBehaviour of complete). At this point, the shortest path from the visual state of hover to the selected state of normal is the direct edge back to normal, so this will be triggered and another animation will occur over 2 seconds.

Instantaneous reversal

If interruptBehaviour is given a value of reverse, then a shortest path calculation occurs as soon as a selected state change occurs. This calculation will either generate a path in which the initial segment is a completion of the current animation, or a path in which the initial segment is a reversal of the current animation. Both the forward and reverse initial segment will conserve the original timing of the edge - for example a 2 second animation interrupted after 0.5 seconds will take 1.5 seconds to complete in the forward direction and 0.5 seconds to complete in the reverse direction.

(We think there’s a use case for a blend value as well, where shortest path calculation occurs from the destination state of the current edge, and the remaining animation between the current visual state and the destination state is blended with the first edge or edges of the shortest path. This also introduces the possibility of reverseBlend and bothBlend).

Explicit Nodes

Chaining animations edges together introduces a complication: it's not in general possible to go from a particular target state back to a style for that state. This happens because in CSS, a particular DOM state generates a set of matching CSS Rules that dictate style property values for that DOM; and because states are represented in this proposal by property values. Hence, if we need to work out the set of style property values that apply to a particular state, we'd need to work out a DOM state that would generate a set of matching CSS Rules that would set that particular state.

This is important with chained animations as often an intermediate state will need to be resolved as the target of a shortest-path edge. Rather than attempting to infer the "correct" DOM state, we propose introducing an "@node" rule that contains a set of default property values that will apply when generating intermediate states.

The syntax for the @node rule is as follows:

@transition-graph .item {
  over: state-item;
  @edge(A, B) { ... }
  @edge(B, C) { ... }
  @node(B) { prop1: value1; prop2: value2; ... }
}

The effect of this declaration is to introduce a new style rule that applies when the value of state-item is B and has a specificity lower than every other rule. When transitioning between state A and state C, the style of intermediate state B is generated by taking the destination style of C, and replacing any transitioning properties with values provided in the @node block, if present.