Summary#
An animation declares motion that loops with no destination state — a skeleton shimmer, a pulse
on a live indicator, an indeterminate progress hint while a long job runs, or a highlight that decays back
after a row changes.
That is the gap it fills. A transition already covers every A→B state change: hover a button and its tint moves, toggle a drawer and it slides. But some motion has no "to" value — a reconnecting spinner runs until the connection comes back, and a "this row just changed" flash has to fade away by itself. Those are events, not states, so there is nothing for a transition to move toward.
animation Shimmer {
Duration = "1.5s";
Easing = EaseInOut;
Repeat = Infinite;
0% { Opacity = 0.4; }
100% { Opacity = 1; }
}A keyframe stop's body is the ordinary style-prop vocabulary — the same names a variants block uses. So a
stop is checked at compile time, a misspelled prop is an error rather than a silently dead line, and a stop can
read a theme token like any other style.
Signature#
animation <Name> {
Duration = "<time>"; // optional — "1.5s" or "600ms"
Easing = <Linear|Ease|EaseIn|EaseOut|EaseInOut>; // optional
Repeat = <Infinite|<count>>; // optional — Infinite, or a whole number
<percent>% { <StyleProp> = <value>; … } // one block per stop
from { … } // an alias for 0%
to { … } // an alias for 100%
}Description#
Where an animation is declared — top level, not in a theme#
An animation is app-global, declared at the top level beside entity, enum and component. It is not part
of a theme: a theme holds tokens, which are single values, while an animation is a rule with structure of
its own. And it is not declared inside a component, because motion like a shimmer is reused across many screens —
declaring it once is the point.
Where do Duration, Easing and Repeat go?#
Duration, Easing and Repeat are written once, where the animation is defined — not at every place that
uses it. A use site is therefore a single reference, and reading it tells you the whole story:
component Skeleton() {
variants { base { Bg = Colors.Surface; Animation = Shimmer; } }
render { Box(h: "16px"); }
}All three settings are optional; anything you omit takes the platform's default (run once, at an even pace).
Easing is a fixed set of words: Linear, Ease, EaseIn, EaseOut, EaseInOut. A typo is a compile error
that lists the accepted words.
Repeat takes Infinite — the usual choice, because an animation exists for motion that runs until the work
ends — or a whole number for a fixed number of passes (Repeat = 3).
Duration is written as a time string: "1.5s" or "600ms". A bare number is refused, because it would
be ambiguous between seconds and milliseconds.
Describing the keyframes — stop blocks#
Each stop says where in the run it applies and what is true there. Write them in any order — they run from 0% to 100% regardless:
animation Pulse {
Duration = "1.4s";
Easing = EaseInOut;
Repeat = Infinite;
50% { Opacity = 0.45; }
0% { Opacity = 1; }
100% { Opacity = 1; }
}from and to are accepted as aliases for 0% and 100%, so a keyframe set copied out of a stylesheet reads
the same here:
animation SlideIn {
Duration = "200ms";
Easing = EaseOut;
from { TranslateX = "-100%"; }
to { TranslateX = "0"; }
}A stop holds style props only — a flat list. Interaction states (Hover, Focus) and responsive overrides
belong in a component's variants block; they have no meaning partway through an animation.
Using theme tokens in a stop#
Because stops use the ordinary style props, they can reference theme tokens. Motion then re-themes with everything else — switching theme or color mode changes the animation with no code change:
theme T { Colors { Surface = "#FFFFFF"; Accent = "#0077B6"; } }
animation Flash {
Duration = "900ms";
Easing = EaseOut;
0% { Bg = Colors.Accent; }
100% { Bg = Colors.Surface; }
}Putting an animation on an element — the animation prop#
Reference the animation by name with the animation style prop — either as an inline argument or in a variants
block:
component Status() {
render { Text("Reconnecting…", animation: Pulse); }
}The name is checked when you compile. Referring to an animation that does not exist is an error naming the ones that do — because the alternative failure has no symptom at all: the element renders perfectly and simply never moves.
Staggering: animationDelay#
Two elements running one animation are in lockstep — which is right for a pair of skeleton rows and wrong for a
chase: a row of bulbs lighting in sequence, a wave across a bar chart, a spinner made of dots. That is one
animation with a per-element offset, and the offset is the animationDelay style prop:
animation Bulb {
Duration = "1.2s";
Easing = EaseInOut;
Repeat = Infinite;
0% { Opacity = 0.25; }
50% { Opacity = 1; }
100% { Opacity = 0.25; }
}
component Marquee() {
render {
Row(gap: 2) {
foreach (var i in Enumerable.Range(0, 6)) {
Box(w: 12, h: 12, rounded: 99, bg: "#F5C518", animation: Bulb, animationDelay: (i * 150) + "ms");
}
}
}
}Six bulbs, one declaration. Without the delay this is six animation declarations that differ only in where their
keyframes sit — which is what it used to cost.
The value is a time string with its unit: "200ms" or "0.2s". A bare number is refused for the same reason
Duration refuses one — it is ambiguous between seconds and milliseconds, and CSS reads an unsuffixed number as
neither, so the declaration would be silently dropped. Any expression in scope may build it ((i * 150) + "ms"
above), so the offset can come from a loop index, a parameter or state.
A negative delay starts the animation already partway through, which is how you get a chase that is fully
running on the first frame rather than filling in over the first cycle: animationDelay: (i * -150) + "ms".
When not to reach for one#
If the motion has a destination — a color that settles, a panel that finishes opening, a button that grows on
hover — use a transition instead. It is simpler, it interrupts and reverses cleanly when the state changes
again, and it is what the state-change case is for. Reach for an animation when there is nothing to settle on.
See also#
- theme tokens — the design tokens a keyframe stop can read.
- style props — the style-prop vocabulary a stop's body is written in.
- component —
variants, and where a use site lives.