Timeline
$flow.timeline() is a client-side API for orchestrating multi-step animations. Unlike flowAnimate() which runs a single transition from the server, timelines let you chain steps, run actions in parallel, loop sequences, and lock interactivity during playback -- all from Alpine expressions in your Blade templates.
Basic usage
Build a timeline with $flow.timeline(), add steps, then call .play():
<button
x-on:click="
$flow.timeline()
.step('a', { position: { x: 300, y: 0 } }, 500)
.step('b', { position: { x: 600, y: 0 } }, 500)
.step('c', { position: { x: 450, y: 200 } }, 500)
.play()
"
>
Play Sequence
</button>
Each .step(nodeId, changes, duration) animates a single node over the given duration in milliseconds. Steps execute sequentially -- each waits for the previous one to finish.
step()
Animate a single node's properties over a duration:
<button x-on:click="
$flow.timeline()
.step('node-1', { position: { x: 400, y: 100 } }, 600)
.step('node-1', { data: { label: 'Arrived!' } }, 300)
.play()
">
Move then relabel
</button>
parallel()
Run multiple animations at the same time. Pass an array of step definitions:
<button x-on:click="
$flow.timeline()
.parallel([
['a', { position: { x: 200, y: 0 } }, 600],
['b', { position: { x: 400, y: 0 } }, 600],
['c', { position: { x: 300, y: 200 } }, 600],
])
.play()
">
Move all at once
</button>
You can mix sequential steps and parallel blocks:
<button x-on:click="
$flow.timeline()
.step('a', { position: { x: 100, y: 0 } }, 400)
.parallel([
['b', { position: { x: 400, y: 0 } }, 500],
['c', { position: { x: 400, y: 200 } }, 500],
])
.step('a', { position: { x: 400, y: 100 } }, 400)
.play()
">
Sequential then parallel then sequential
</button>
loop()
Repeat the entire timeline a set number of times, or pass Infinity to loop forever (until the page navigates away or you call stop):
{{-- Loop 3 times --}}
<button x-on:click="
$flow.timeline()
.step('a', { position: { x: 300, y: 0 } }, 500)
.step('a', { position: { x: 0, y: 0 } }, 500)
.loop(3)
.play()
">
Bounce 3 times
</button>
lock()
Prevent user interaction (pan, zoom, drag) while the timeline is playing. Interaction is restored automatically when playback finishes:
<button x-on:click="
$flow.timeline()
.lock()
.step('a', { position: { x: 500, y: 0 } }, 800)
.step('b', { position: { x: 500, y: 200 } }, 800)
.play()
">
Play (locked)
</button>
Edge transitions
Timelines can also animate edges. Use the draw and fade transition styles:
<button x-on:click="
$flow.timeline()
.step('a', { position: { x: 300, y: 0 } }, 500)
.step('e-a-b', { style: { opacity: 1 } }, 400, 'draw')
.step('b', { data: { label: 'Connected!' } }, 300)
.play()
">
Move, draw edge, update label
</button>
Edge transition types:
| Type | Effect |
|---|---|
'draw' |
Edge path draws in progressively from source to target |
'fade' |
Edge fades in from transparent to full opacity |
Easing presets
Pass an easing name as the fourth argument to .step():
<button x-on:click="
$flow.timeline()
.step('a', { position: { x: 400, y: 0 } }, 800, 'easeOutBounce')
.play()
">
Bounce in
</button>
Available presets:
| Easing | Description |
|---|---|
'linear' |
Constant speed |
'easeIn' |
Accelerate from zero |
'easeOut' |
Decelerate to zero |
'easeInOut' |
Accelerate then decelerate |
'easeOutBounce' |
Bounce at the end |
'easeOutElastic' |
Elastic overshoot |
'easeOutBack' |
Slight overshoot then settle |
x-flow-timeline directive
For reactive timelines that rebuild when data changes, use the x-flow-timeline directive. The timeline replays automatically when the bound expression changes:
<div x-data="{ step: 0 }">
<x-flow :nodes="$nodes" :edges="$edges" style="height: 400px;">
<x-slot:node>
<x-flow-handle type="target" position="top" />
<span x-text="node.data.label"></span>
<x-flow-handle type="source" position="bottom" />
</x-slot:node>
{{-- Timeline replays when step changes --}}
<template x-flow-timeline="step">
<script type="application/json" x-text="JSON.stringify([
{ id: 'a', changes: { position: { x: step * 200, y: 0 } }, duration: 500 },
{ id: 'b', changes: { position: { x: step * 200 + 300, y: 0 } }, duration: 500 },
])"></script>
</template>
</x-flow>
<div class="mt-4 flex gap-2">
<button x-on:click="step++" class="rounded bg-blue-500 px-3 py-1 text-sm text-white">Next Step</button>
<button x-on:click="step = 0" class="rounded bg-gray-500 px-3 py-1 text-sm text-white">Reset</button>
</div>
</div>
Complete example
A Livewire component that triggers a client-side timeline from server data:
<?php
namespace App\Livewire;
use Livewire\Component;
class TimelineDemo extends Component
{
public array $nodes = [
['id' => 'a', 'position' => ['x' => 0, 'y' => 0], 'data' => ['label' => 'Start']],
['id' => 'b', 'position' => ['x' => 300, 'y' => 0], 'data' => ['label' => 'Middle']],
['id' => 'c', 'position' => ['x' => 600, 'y' => 0], 'data' => ['label' => 'End']],
];
public array $edges = [
['id' => 'e-a-b', 'source' => 'a', 'target' => 'b'],
['id' => 'e-b-c', 'source' => 'b', 'target' => 'c'],
];
public function render()
{
return view('livewire.timeline-demo');
}
}
{{-- resources/views/livewire/timeline-demo.blade.php --}}
<div>
<div class="mb-4 flex gap-2">
<button
x-on:click="
$flow.timeline()
.lock()
.step('a', { position: { x: 0, y: 100 } }, 400, 'easeOut')
.step('e-a-b', { style: { opacity: 1 } }, 300, 'draw')
.step('b', { position: { x: 300, y: 100 } }, 400, 'easeOut')
.step('e-b-c', { style: { opacity: 1 } }, 300, 'draw')
.step('c', { position: { x: 600, y: 100 } }, 400, 'easeOut')
.play()
"
class="rounded bg-blue-500 px-3 py-1 text-sm text-white"
>
Play Timeline
</button>
<button
x-on:click="
$flow.timeline()
.parallel([
['a', { position: { x: 0, y: 0 } }, 500],
['b', { position: { x: 300, y: 0 } }, 500],
['c', { position: { x: 600, y: 0 } }, 500],
])
.play()
"
class="rounded bg-gray-500 px-3 py-1 text-sm text-white"
>
Reset
</button>
</div>
<x-flow :nodes="$nodes" :edges="$edges" :fit-view-on-init="true" style="height: 400px;">
<x-slot:node>
<x-flow-handle type="target" position="left" />
<span x-text="node.data.label"></span>
<x-flow-handle type="source" position="right" />
</x-slot:node>
</x-flow>
</div>
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 0, y: 60 }, data: { label: 'Step 1' } },
{ id: 'b', position: { x: 0, y: 60 }, data: { label: 'Step 2' } },
{ id: 'c', position: { x: 0, y: 60 }, data: { label: 'Step 3' } },
],
edges: [],
background: 'dots',
fitViewOnInit: true,
controls: false,
})" class="flow-container" style="height: 250px;"
x-init="
const playBtn = document.getElementById('demo-seq-play');
const resetBtn = document.getElementById('demo-seq-reset');
if (playBtn) playBtn.addEventListener('click', () => {
$flow.timeline()
.step({ nodes: ['a'], position: { x: 0, y: 60 }, duration: 400 })
.step({ nodes: ['b'], position: { x: 200, y: 60 }, duration: 400 })
.step({ nodes: ['c'], position: { x: 400, y: 60 }, duration: 400 })
.play();
});
if (resetBtn) resetBtn.addEventListener('click', () => {
$flow.update({ nodes: {
a: { position: { x: 0, y: 60 } },
b: { position: { x: 0, y: 60 } },
c: { position: { x: 0, y: 60 } },
}});
$flow.fitView({ duration: 300 });
});
">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source></div>
</div>
</template>
</div>
</div>
Related
- Update & Animate -- server-side flowUpdate/flowAnimate
- Particles -- fire particles along edges
- Camera Control -- focus and follow nodes