Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

animationKey — play an animation again when a value changes; a line that draws itself

Box(animation: KitNudge, animationKey: count) { … } · Box(animation: KitDraw) { Icon(Icons.Check); } · Done("Saved")

`animationKey: value` on any element makes its animation play each time the value CHANGES — and not when the element first appears, so a page load nudges nothing and the count that later moves is the one the eye goes to. `StrokeDasharray`/`StrokeDashoffset` (and the kit's `KitDraw`) make a line draw itself: a check that writes itself when something lands.

preview1 example compiled by CIuianimationmotion

Summary#

An animation plays when its element appears. A COUNT wants the opposite: it should move when the number moves, and never on a page load, when nothing changed. animationKey: value says exactly that — the element's animation plays again each time value changes, and is held still when the element first appears (server paint included). The kit's nav badges, the shell's notification count and its pin all nudge this way.

A line that DRAWS itself — the tick that writes itself when a save lands — is two style props and one kit animation: StrokeDasharray/StrokeDashoffset hide a stroke behind a dash and slide the dash along it, and KitDraw does that for any line up to 24 units of the icon grid. Done("Saved") is the tick and its word, ready-made.

Signature#

Badge(unread.ToString(), animation: KitNudge, animationKey: unread);           // nudges when `unread` changes
Box(animation: KitDraw) { Icon(Icons.Check); }                                  // the tick draws as it appears
Box(animation: KitDraw, animationKey: savedAt) { Icon(Icons.Check); }           // …and again on every save
Done("Saved")                                                                   // a drawn tick and its word

Description#

Plays on the change, not on the first appearance#

animationKey: takes any value — a number, a string, a bool, an enum — and is accepted on every atom, like animation: itself. When the value changes between two renders, the element's animation restarts from its first frame; a re-render that leaves the value alone does nothing. On the element's FIRST appearance the animation is held off — nothing changed then, and a screen of twenty counts that all nudge on load hides the one that later moves. An element that should also ENTER is wrapped: the entrance on the outer box, the key on the inner one.

On a component call (Badge, Card) both go on the call, and land on the component's root element (Placing and naming a component — m:, w:, grow:, alignSelf:, animation:, label: on a component call): Badge(n.ToString(), animation: KitNudge, animationKey: n).

A line that draws itself#

StrokeDasharray sets the dash pattern of every line drawn inside the element and StrokeDashoffset how far along the line the pattern starts; both INHERIT, so on the box around an Icon they reach the icon's own strokes. A dash as long as the line, offset by that length, hides it; animating the offset to 0 draws it. KitDraw is exactly that (24 → 0 over 320ms), sized for the 24-unit icon grid — every tick and circle-tick in the set draws whole.

Done(label = "Saved", tone = Tone.Success) is a tick that draws as it appears beside its word, announced politely (role: Status). The kit's Checkbox draws its tick when it is ticked — keyed to its value, so a form of ticked boxes does not draw on load.

Reduced motion#

Nothing to write. A nudge is a scale, which reduced motion drops; a stroke's travel (StrokeDashoffset) is dropped too, so the tick is simply there. The key still changes; only the movement is gone.

Examples#

[Route("/")]
[AllowAnonymous]
component Queue() {
  int waiting = 3;
  int saves = 0;
  action Arrive() { waiting = waiting + 1; }
  action Save() { saves = saves + 1; }
  render {
    Stack(gap: 3, p: 6) {
      Row(gap: 2, align: Align.Center) {
        Text("Waiting on you");
        // Key it to the count: it nudges when a ticket arrives, never when the page loads.
        Badge(waiting.ToString(), tone: Tone.Warning, animation: KitNudge, animationKey: waiting);
      }
      Button("Simulate a ticket", onPress: Arrive);
      Row(gap: 2, align: Align.Center) {
        Button("Save", onPress: Save, tone: Tone.Primary);
        // A drawn tick, again on every save.
        if (saves > 0) {
          Box(animation: KitDraw, animationKey: saves, color: Colors.Success) { Icon(Icons.Check, size: 18); }
        }
      }
      if (saves > 0) { Done(); }
    }
  }
}

Testing them#

Text assertions see the words, not the motion: Assert.Visible("4") after the press, Assert.Visible("Saved"). Whether the motion plays is a real-browser question — osy test --pixels photographs the page, and the platform's own visual gate checks the replay mechanism.

See also#

Related

animation — keyframe motion, on entry, on exit, in a loop, or as it scrolls into view

An `animation` block declares reusable keyframe motion — an entrance, a shimmer, a pulse, a flash that decays. Its…

style props

Inside a `variants` block, each `Name = value` is a style prop from a fixed vocabulary the renderer maps to CSS — paint…

Pinned and recent pages, the app switcher, and presence in a shell

A work shell draws the reader's own shortcuts — the pages they PINNED and the ones they opened RECENTLY — above its…

Osysharp.Ui (the UI kit)

The bundled UI kit — ready-made styled controls like `Button`, the shared design-system vocabularies (`Tone`, `Size`)…