[[Authors Note: This post is a lightly edited copy of an essay I wrote back in Feb 2011 for internal Google brainstorming/discussion. I recently did some cleanup of some of my documents, and decided it would be a useful thing to share publicly.]]
This is a proposal to make CSS Transitions and Animations more powerful, and unify the processing models.
CSS divides animation into two categories - Transitions, which run when a property changes its value, and Animations, which run forever as long as an element is in a particular state.
Animations are fine. They do what they need to do. No change necessary (though we do still want to check and see if there are any additional features we want to add).
Transitions are far, far too weak. There are two major weaknesses of current Transitions:
- Transitions can only manipulate the property that they are firing on. You cannot, for example, make an element wobble up and down while transitioning the 'left' property, or make an element fade in or shutter itself when its display changes to/from 'none'.
- Transitions fire the same animation for all property value changes. You cannot discriminate between changes, firing one animation between values A and B, but another animation when going from B to C, or B back to A. For example, creating an element with a “CRT” effect may involve growing it to the full size quickly and fading from white to normal when going from display:none to display:block, but doing “shrink height, then shrink width, then fade” when going from display:block to display:none.
Letting Transitions Manipulate More Than Just the Property They Fire On
This is a relatively easy fix. We already have keyframes for specifying the effect of an animation over several properties at once. We just need to apply that to transitions. As well, currently keyframes are always “absolute” - they don't refer to any values outside of themselves, so you can't use keyframes to create a complex timing function between arbitrary values.
So, first, introduce from(), to(), and prev() functions which can be used inside of keyframes. They return the value that property had at the start of the animation, the end of the animation, or the last keyframe that set that property, respectively. This can then be used in calc() to slightly alter values.
For example:
p { transition: left wobble .5s; } @keyframes wobble { 0% { top: from(); left: from(); 25% { top: calc( from() + 50px ); } 50% { top: from(); } 75% { top: calc( from() - 50px ); } 100% { top: from(); left: to(); } }
(The 'left' declarations aren't strictly necessary - they'll be inferred since it's already transitioning on 'left'. But you can override them to supply your own timing function, as trivially illustrated here.)
Second, let the 'transition' property take a keyframe value as well.
Specifying Transitions Between Specific Values
This is a touch more complicated. Imagine a graph with each value of a property being a node, and the edges being the transitions. The 'transition' property sets all the edges to the same animation. We want to be able to set specific edges to specific animations.
To do this, we will introduce a new @transition rule, like so:
@transition article > section { over: display; from: none; to: block; animation: shutter-open .2s; } @transition article > section { over: display; from: block; to: none; animation: shutter-closed .2s; } The general syntax is: @transition [selector] { over: [property name]; from: [property value]; to: [property value]; animation: [animation specifier, per the Animations spec]; }
The 'transition' property and @transition rules may be combined over the same property. The animation defined by the 'transition' property sets the animation for any edge that isn't explicitly defined by a @transition rule. (Properly, 'transition' desugars into a @transition rule with from:*; to:*;
, which has lower specificity than any other @transition over the same property.)
Taken together, the set of @transition rules that apply to an element form a “transition graph”. We can then operate on the transition graph to reduce the busywork involved. For example, if we define a transition from value A to value B, and from value B to value C, then if the value changes directly from A to C we can figure out that we should go through value B without forcing the author to explicitly create a third @transition. (There may be multiple paths through the transition graph, in which case we need a notion of “shortest path”. How is distance measured - number of hops between nodes, total duration of the combined transition, etc.? Please, no complaints about the TSP - in practice this should be tractable.)
Also, in many simple cases you'll be okay with using the same transition for A->B and from B->A. It's a shame to have to manually specify both transitions, so you can specify that a transition should be used for both directions of an edge:
@transition button { over: state-disabled; from: active; to: disabled; animation: transition(opacity) .2s; direction: both; /* values: forward | both */ }
(This also uses a proposed transition() function, which takes any number of properties and automatically defines a keyframe set from the start value to the end value of each property. This one, for example, is identical to defining a keyframe like:
@keyframes fade-opacity { 0% { opacity: from(); } 100% { opacity: to(); } }
)
Given the above changes, a number of additional improvements suggest themselves.
Transitioning Based On Application-Defined State Changes
[[Author's note: This document was originally written back in Feb 2011, which means it preceded my work on the CSS Variables spec. Today, I'd just call this Variables and be done with it. However, they may still be interesting stuff in here - for example, non-inheriting variables might be useful for this.]]
Specifying transitions on property value changes is well and good, but sometimes the thing that's changing is some internal application/component-specific state, not a specific property. For example, when dealing with buttons, you want to transition between interaction states based on whether the mouse is hovering or clicking the button. These will have CSS effects (maybe changing color, shadow, position, etc.) but no single property captures the idea of changing between these states (you may also change color when disabling the button, so you can't hook it to that, etc.).
To solve this, we introduce a set of author-extensible properties for use in specifying application state and firing transitions, like so:
button { state-interaction: normal; } button:hover { state-interaction: over; } button:hover:active { state-interaction: down; } button:active { state-interaction: out; } @transition button { over: state-interaction; from: normal; to: over; animation: foo .5s; } /* etc. */
Authors are free to define any property following the pattern “state-*” with any values. These properties are guaranteed to have no direct CSS effects; they're provided solely to hook transitions off of.
(This particular set of states will likely be ridiculously common. We should probably define a state-* value for this in the UA stylesheet, so authors can hop directly to defining @transitions on it without having to go through the busywork of writing the four blocks every single time.)
Advanced Control Over Mid-Transition Interruptions
If you are in the middle of a transition when the property that fired the transition changes again, either back to the original value or to a new third value, there are several choices about how to proceed. Let's split these into two cases:
The value was changed back to the starting value
When this occurs, there are two reasonable actions:
- Reverse the animation you are currently on.
- Finish the current transition, then pathfind a new transition back to the new(/original) state.
The author should be able to specify which of these to use, as both are valuable. For example, if an animation is used to make a train travel around a track to where the mouse is, you always want the train to move forward, not to skip backwards when the user jiggles their mouse. On the other hand, if you are animating the border-color of a button on hover and the user merely passes their mouse over the button on the way to something else, you'd prefer the animation to quickly and unobtrusively reverse any progress it made in the short amount of time it was transitioning.
This can be accomplished through a switch on the @transition:
@transition button { over: state-transition; from: normal; to: over; animation: hover-button .2s; interrupt: reverse; /* values: reverse | ignore */ }
(The naming here is horrible, but it's the best I could come up with. Someone suggest something better?)
Which value should be the default? I suspect that most people would want reversibility by default.
The Value Was Changed To a Third, Different Value
This situation also has two reasonable resolutions, similar to the other case:
- Stop the current transition, interpolate a brand new one from
to automagically. - Finish the current transition, then pathfind a new transition to the new state.
Option #2 is actually identical to option #2 in the previous situation. Option #1 seems similar, but it's not - the previous situation just reverses the running transition, while in this situation you automatically create a new transition on the fly.
I haven't decided on a good syntax here, or even on the the precise interaction model. Some points to think about:
- Is this necessary to expose per-transition, or can it just be set on an element as a default “I've been interrupted while transitioning” strategy?
- What properties does the interrupt transition move over?
- In general, what should be specified by the author for the interrupt transition? Duration seems obvious, and maybe timing function, but what about keyframes?
- A sense of 'momentum' can be attractive, where the original transition doesn't abruptly stop and give way to the interrupt transition, but rather the effects of the original transition continue and are gradually overrided by the new transition. This is easy to do automatically, and can be specified with a higher-level timing function. The default timing function can be 'step-end', which just immediately switches to the interrupt transition, but specifying 'ease-in-out' would make a smooth transition.
Suggestions? I'm thinking a property with a similar syntax to 'transition'. A per-transition interrupt can be done manually by simply adding more intermediate states which branch off into additional “mid-way” transitions.
Open-Ended Transitions
Sometimes you don't necessarily need to care what the start state of a transition is, you just want to run a particular transition anytime you change to a particular state. Similarly, the reverse can be useful. The @transition syntax allows you to do this:
@transition article > section { over: display; to: none; animation: fade .2s; }
The value 'initial' for the 'from' or 'to' property is magical and means “any state”. You can just omit the 'from' or 'to' to get the same effect.
(Alternately, we define that the initial value is '*', a non-ident that is likely to never be a property value.)
Given this, then, we can finally fully desugar the 'transition' property. Given something like <selector> { transition: <property> <other stuff>; }
, this could instead be done as:
@transition <selector> { over: <property>; animation: transition(<property>) <other stuff>; interrupt: reverse; /* Maybe this is the default value */ }
Cool stuff! When do you think the time is right to start this? I'm especially keen on transitions between specific values since it has a lot of power.
Reply?
I really like where you're going with this, but I'm not really a fan of the syntax proposed.
There happens to be a lot of folk out there with cms-based sites (WordPress) that have been extended by way of tons of 3rd party plugins. This leads a natural tradeoff fo performance-optimization vs just-get-things-done. While ugly, it does highlight the strength/stability of the web platform, and especially CSS.
I think many of us have brought into projects that have become waist-deep in compatibility problems from the compounding of scripts and stylesheets. In these situations, it's comforting to see how traceable styles are in all of the browsers. As we continue to extend CSS, I worry that we lose that aspect, the clean traceability of styles. It seems that we want to continue to move more of flexibility of functional languages to CSS, because it's easier to understand things in declarative land. Also, this gives us the developer the comforting feeling that performance is in the browser's hands as long as we're only declaring things.
But, as we do that, I hope that we don't make CSS too functional.
Reply?
Testing hello world
Reply?