# animation — looping motion with no destination state

> An `animation` block declares reusable, looping motion — a shimmer, a pulse, an indeterminate progress hint. Its keyframe stops are written with the ordinary style props, so they are checked at compile time and can read theme tokens. The timing lives on the declaration, so a use site is a single `animation: Name` reference.

<!-- id: ui-animation · area: ui · stability: stable · html: https://osysharp.com/reference/ui/animation/ -->

## Summary        {#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*.

```osy title="a skeleton shimmer" test app=ui-animation
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      {#signature}
```osy syntax
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    {#description}

### Where an `animation` is declared — top level, not in a theme   {#placement}
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?   {#timing}
`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:

```osy title="one reference — the timing comes with it" test app=ui-animation
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   {#stops}
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:

```osy title="a three-stop pulse; declared out of order on purpose" test app=ui-animation
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:

```osy title="from / to" test app=ui-animation
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    {#tokens}
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:

```osy title="a stop reading theme tokens" test app=ui-animation
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   {#applying}
Reference the animation by name with the `animation` style prop — either as an inline argument or in a `variants`
block:

```osy title="inline, on any element" test app=ui-animation
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`    {#delay}
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:

```osy title="one animation, N elements, a chase" test app=ui-animation
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    {#when-not}
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   {#see-also}
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the design tokens a keyframe stop can read.
- [style props](https://osysharp.com/reference/ui/styling/) — the style-prop vocabulary a stop's body is written in.
- [component](https://osysharp.com/reference/ui/component/) — `variants`, and where a use site lives.
