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.
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.
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.
{#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.
<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
.flshclass-body animation form — animations are tied to a template's element ids, which a class has none of. - ○
repeat: trueis rejected on anout:/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/slidepresets reject a target with a dynamictranslation— use a customanimation {}declaration instead. - ○
scaleanimations 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 anout:animation — a block with no transition, or anin:-only transition, leaves its focusable content registered indefinitely (a registry-hygiene concern; it can no longer actually receive focus, sincenavigate()'s own visibility check already excludes it).
Exact grammar: GRAMMAR.md. Full feature status: docs/features.md.