🎬

Animation
animation {} declarations, transition:/in:/out:, animate:.

Three layers, all compiling straight to Roku's native Animation/SequentialAnimation/ParallelAnimation nodes — no shared runtime helper library, no JS-side animation loop.

Layer 1 — declarations and trigger sugar

A named, reusable animation, triggered imperatively with .start()/.stop()/.pause()/.resume()/.finish(). Known field shorthands: opacity, rotation, translation, scale, color — plus a field: { name, as, ... } escape hatch for anything else.

BounceButtonDemo.thr (excerpt)
animation bounce {
  target: card
  duration: 0.4
  easeFunction: "outExpo"
  scale: [1, 1.15, 1]
}

private function play(key: string, press: boolean) {
  if (press) {
    bounce.start()
  }
}

.onFinish(callback) — animation-finished hook

Runs every time bounce reports state = "stopped" — unlike taskManager.onResult's fire-once shape, this fires on every completion, since an animation like a bounce button's is commonly retriggered. The callback can be a bound function reference or an inline anonymous function; registering it again just replaces the previous one. Rejected on an animation declaring repeat: true anywhere in its own step tree — state never reports "stopped" for a looping animation, so the callback would never fire.

BounceButtonDemo.thr (excerpt)
private function play(key: string, press: boolean) {
  if (press) {
    bounce.start()
    bounce.onFinish(onBounceDone)
  }
}

private function onBounceDone() {
  state bounceCount = bounceCount + 1
}

Composition

sequential: true or parallel: true plus a steps: [...] array — arbitrarily nestable.

animation introSequence {
  target: card
  sequential: true
  steps: [
    { opacity: [0, 1], duration: 0.3 },
    { translation: { key: [0, 1], keyValue: [[0, 40], [0, 0]] }, duration: 0.4 }
  ]
}

Layer 2 — transition:/in:/out:

Enter/exit animation for a {#if}/{#if:destroy} block's content. <name> is a built-in preset (fade/fly/slide/scale) or a script-declared animation — visibility/removal is deferred until the exit animation actually finishes.

TogglePresetDemo.thr (excerpt)
{#if showPanel}
<Rectangle id="panel" transition:fade="{{duration: 0.3}}" focusable="true"
           on:key[OK]="{togglePanel()}">
  <Label id="panelLabel" text="Panel — press OK to hide (fades out)" />
</Rectangle>
{/if}
{#if:destroy showCard}
  <Poster id="card" in:bounce out:fade="{{duration: 0.15}}" />
{/if}

Layer 3 — animate:<field>

Auto-animates a reactive cascade's own write to a matching dynamic attribute, instead of an instant snap. The element's initial value at mount still snaps instantly — only a later, cascade-triggered write animates.

AnimateAttrDemo.thr (excerpt)
<Poster id="poster" opacity="{posterOpacity}" animate:opacity="{{duration: 0.4}}" />

posterOpacity here is a plain state, flipped explicitly between two levels — a ternary can't be used directly inside a dynamic attribute's own expression, only as the whole right-hand side of an assignment or state write.

Also usable on <FlashTheaterRouterOutlet>

navigate-out:/navigate-in:/back-out:/back-in: attach a preset or declared animation to a router outlet's own mount/unmount swap, the same value grammar as transition:/in:/out: above — see Router — Router-outlet transitions.

Reference implementation — apps/animation-demo

Every mechanism on this page has a router-mounted, scaled chapter in apps/animation-demo — 8 chapters (/declared through /outlet-transitions), reachable with REWIND/FAST-FORWARD once compiled and sideloaded. Each chapter shows a default, no-customization example alongside a deliberately different, customized one — e.g. BounceButtonDemo.thr's OK-triggered bounce (defaults) next to its Replay-triggered customized animation (a different easeFunction, the color shorthand, and the field/as escape hatch, all in one declaration). This is the live, compiling reference for the whole page — see findings/animation-demo-app.md for what each chapter covers and findings/demo-app-conventions.md for the app-structure convention it follows.

⚠️ Not (yet) supported

  • ○ No .flsh class-body animation form — animations are tied to a template's element ids, which a class has none of.
  • ○ repeat: true is rejected on an out:/exit transition, and on a .onFinish(...) target — a repeating animation never reports "stopped" on its own, so the block would never actually hide, and the callback would never fire.
  • ○ fly/slide presets reject a target with a dynamic translation — use a custom animation {} declaration instead.
  • ○ scale animations are not reflected in the focus system's LRUD geometry — a card animating its own scale won't resize its hit-testing footprint.
  • ○ A toggle-mode ({#if}, non-destroy) block's focusable content is only unregistered on hide if the block declares an out: animation — a block with no transition, or an in:-only transition, leaves its focusable content registered indefinitely (a registry-hygiene concern; it can no longer actually receive focus, since navigate()'s own visibility check already excludes it).

Exact grammar: GRAMMAR.md. Full feature status: docs/features.md.