Timeline

The timeline API sequences multi-step animations with parallel execution, looping, lock mode, and edge transitions. Timelines can be created programmatically via $flow.timeline() or declaratively with the x-flow-timeline directive.

<div x-data="flowCanvas({
    nodes: [
        { id: 'a', position: { x: 200, y: 60 }, data: { label: 'Input' } },
        { id: 'b', position: { x: 200, y: 60 }, data: { label: 'Process' } },
        { id: 'c', position: { x: 200, y: 60 }, data: { label: 'Output' } },
    ],
    edges: [],
    background: 'dots',
    controls: false,
    pannable: false,
    zoomable: false,
})" class="flow-container" style="height: 220px;"
   x-init="
       const origin = { x: 200, y: 60 };
       document.getElementById('demo-intro-play').addEventListener('click', () => {
           $flow.timeline()
               .lock()
               .parallel([
                   { nodes: ['a'], position: { x: 20, y: 60 }, duration: 500, easing: 'easeOut' },
                   { nodes: ['b'], position: { x: 200, y: 60 }, duration: 500, easing: 'easeOut' },
                   { nodes: ['c'], position: { x: 380, y: 60 }, duration: 500, easing: 'easeOut' },
               ])
               .step({
                   addEdges: [{ id: 'e1', source: 'a', target: 'b' }],
                   edgeTransition: 'draw',
                   duration: 400,
               })
               .step({
                   addEdges: [{ id: 'e2', source: 'b', target: 'c' }],
                   edgeTransition: 'draw',
                   duration: 400,
               })
               .play();
       });
       document.getElementById('demo-intro-reset').addEventListener('click', () => {
           removeEdges(['e1', 'e2']);
           $flow.update({ nodes: {
               a: { position: origin },
               b: { position: origin },
               c: { position: origin },
           }});
       });
   ">
    <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>

Programmatic API

Creating a Timeline

const tl = $flow.timeline();

Chain API

Build a timeline by chaining step(), parallel(), loop(), lock(), and play():

tl.step({ nodes: ['a'], position: { x: 100 }, duration: 500 })
  .step({ nodes: ['b'], position: { x: 200 }, duration: 300 })
  .parallel([
      { nodes: ['c'], position: { x: 300 }, duration: 400 },
      { nodes: ['d'], position: { x: 400 }, duration: 400 },
  ])
  .loop(2)    // loop 2 times (0 = infinite)
  .lock()     // disable user input during playback
  .play();

Click play to see each node move in sequence:

<div x-data="flowCanvas({
    nodes: [
        { id: 'a', position: { x: 10, y: 10 }, data: { label: 'Step 1' } },
        { id: 'b', position: { x: 10, y: 70 }, data: { label: 'Step 2' } },
        { id: 'c', position: { x: 10, y: 130 }, data: { label: 'Step 3' } },
    ],
    edges: [],
    background: 'dots',
    controls: false,
    pannable: false,
    zoomable: false,
})" class="flow-container" style="height: 250px;"
   x-init="
       document.getElementById('demo-seq-play').addEventListener('click', () => {
           $flow.timeline()
               .step({ nodes: ['a'], position: { x: 350, y: 10 }, duration: 400, easing: 'easeOut' })
               .step({ nodes: ['b'], position: { x: 350, y: 70 }, duration: 400, easing: 'easeOut' })
               .step({ nodes: ['c'], position: { x: 350, y: 130 }, duration: 400, easing: 'easeOut' })
               .play();
       });
       document.getElementById('demo-seq-reset').addEventListener('click', () => {
           $flow.update({ nodes: {
               a: { position: { x: 10, y: 10 } },
               b: { position: { x: 10, y: 70 } },
               c: { position: { x: 10, y: 130 } },
           }});
       });
   ">
    <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>

TimelineStep Properties

interface TimelineStep {
    // Node targeting
    nodes?: string[];
    position?: Partial<XYPosition>;
    dimensions?: Partial<Dimensions>;
    style?: string | Record<string, string>;
    class?: string;
    data?: Record<string, any>;
    selected?: boolean;
    zIndex?: number;

    // Path-based motion
    followPath?: PathFunction | string;  // SVG path string or function
    guidePath?: { visible?: boolean; class?: string; autoRemove?: boolean };

    // Edge targeting
    edges?: string[];
    edgeColor?: string;
    edgeStrokeWidth?: number;
    edgeClass?: string;
    edgeLabel?: string;
    edgeAnimated?: boolean | EdgeAnimationMode;

    // Edge lifecycle
    addEdges?: FlowEdge[];
    removeEdges?: string[];
    edgeTransition?: 'none' | 'draw' | 'fade';

    // Viewport
    viewport?: Partial<Viewport>;
    fitView?: boolean;
    panTo?: string;      // node ID to center on
    fitViewPadding?: number;

    // Parallel container
    parallel?: TimelineStep[];

    // Timing
    duration?: number;
    easing?: EasingName | ((t: number) => number);
    delay?: number;
    lock?: boolean;

    // Hooks
    onStart?: (ctx: StepContext) => void;
    onProgress?: (progress: number, ctx: StepContext) => void;
    onComplete?: (ctx: StepContext) => void;
}

Timeline State

tl.state is a readonly property: 'idle' | 'playing' | 'paused' | 'stopped'.

Timeline Events

The timeline emits events throughout its lifecycle:

Event Description
play Playback started
step A step began executing
step-complete A step finished
pause Playback paused
resume Playback resumed
interrupt Playback was interrupted
complete All steps finished
reverse Direction reversed
loop A loop iteration completed
stop Playback stopped
reset Timeline was reset
tl.on('step-complete', ({ stepIndex }) => {
    console.log(`Step ${stepIndex} done`);
});

Edge Transitions

When adding or removing edges within a timeline step, edgeTransition controls the visual effect:

Transition Effect
'draw' Path traces from source to target (or retracts on remove)
'fade' Opacity fades in/out
'none' Instant appear/disappear (default)

Click play to see edges draw in, then fade out:

<div x-data="flowCanvas({
    nodes: [
        { id: 'a', position: { x: 20, y: 50 }, data: { label: 'Start' } },
        { id: 'b', position: { x: 200, y: 50 }, data: { label: 'Middle' } },
        { id: 'c', position: { x: 380, y: 50 }, data: { label: 'End' } },
    ],
    edges: [],
    background: 'dots',
    controls: false,
    pannable: false,
    zoomable: false,
})" class="flow-container" style="height: 200px;"
   x-init="
       document.getElementById('demo-edge-play').addEventListener('click', () => {
           $flow.timeline()
               .step({
                   addEdges: [{ id: 'e1', source: 'a', target: 'b' }],
                   edgeTransition: 'draw',
                   duration: 600,
               })
               .step({
                   addEdges: [{ id: 'e2', source: 'b', target: 'c' }],
                   edgeTransition: 'draw',
                   duration: 600,
               })
               .step({ duration: 500 })
               .step({
                   removeEdges: ['e1', 'e2'],
                   edgeTransition: 'fade',
                   duration: 400,
               })
               .play();
       });
       document.getElementById('demo-edge-reset').addEventListener('click', () => {
           removeEdges(['e1', 'e2']);
       });
   ">
    <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>

Lock Mode

When lock() is called on the timeline or a step has lock: true, a canvas-level _animationLocked flag disables all user input (drag, pan, zoom, selection, connection, keyboard) during playback. The flag is cleared during pause points and on completion.

Undo/Redo Interaction

History capture is suspended during timeline playback. When the timeline completes (or is interrupted), the entire result is captured as a single undo entry.

Easing Presets

Name Curve
'linear' Constant speed
'easeIn' Quadratic ease-in
'easeOut' Quadratic ease-out
'easeInOut' Quadratic ease-in-out (default)
'easeBounce' Bounce at end
'easeElastic' Elastic overshoot
'easeBack' Slight overshoot in-out

Custom Easing Function

Pass any (t: number) => number function where t goes from 0 to 1:

$flow.animate(targets, {
    duration: 500,
    easing: (t) => t * t * t,  // cubic ease-in
});

x-flow-timeline Directive

Bind a reactive timeline definition from Alpine data for declarative multi-step animations.

Configuration Properties

Property Type Default Description
steps Array [] Array of step() and parallel() entries.
autoplay boolean true Start playback automatically when the directive initializes.
loop boolean | number false Loop playback. true loops indefinitely; a number sets the loop count.
lock boolean false Disable user interaction with the canvas during playback.
speed number 1 Playback speed multiplier. 2 is double speed, 0.5 is half speed.
overflow 'queue' | 'latest' 'queue' Behavior when a new timeline triggers while one is playing.
autoFitView boolean false Automatically fit the viewport at each step.
fitViewPadding number 50 Padding in pixels when autoFitView is enabled.
respectReducedMotion boolean true Skip animations when the OS has reduced motion enabled.

Step Types

Sequential: steps execute one after another.

steps: [
    { nodes: ['a'], position: { x: 100, y: 0 }, duration: 400 },
    { nodes: ['b'], position: { x: 200, y: 0 }, duration: 400 },
    { viewport: { zoom: 1.5 }, duration: 600 },
]

Parallel: wrap multiple steps in a parallel array to run at the same time.

steps: [
    { parallel: [
        { nodes: ['a'], position: { x: 100, y: 0 }, duration: 400 },
        { nodes: ['b'], position: { x: 200, y: 0 }, duration: 400 },
    ]},
    { viewport: { zoom: 1 }, duration: 600 },
]

The timeline advances to the next entry only after all parallel steps complete.

Click play to see both nodes move at the same time:

<div x-data="flowCanvas({
    nodes: [
        { id: 'a', position: { x: 200, y: 10 }, data: { label: 'Node A' } },
        { id: 'b', position: { x: 200, y: 100 }, data: { label: 'Node B' } },
    ],
    edges: [],
    background: 'dots',
    controls: false,
    pannable: false,
    zoomable: false,
})" class="flow-container" style="height: 220px;"
   x-init="
       document.getElementById('demo-par-play').addEventListener('click', () => {
           $flow.timeline()
               .parallel([
                   { nodes: ['a'], position: { x: 20, y: 10 }, duration: 600, easing: 'easeInOut' },
                   { nodes: ['b'], position: { x: 380, y: 100 }, duration: 600, easing: 'easeInOut' },
               ])
               .parallel([
                   { nodes: ['a'], position: { x: 200, y: 10 }, duration: 600, easing: 'easeInOut' },
                   { nodes: ['b'], position: { x: 200, y: 100 }, duration: 600, easing: 'easeInOut' },
               ])
               .play();
       });
       document.getElementById('demo-par-reset').addEventListener('click', () => {
           $flow.update({ nodes: {
               a: { position: { x: 200, y: 10 } },
               b: { position: { x: 200, y: 100 } },
           }});
       });
   ">
    <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>

Element API

The directive exposes a playback API on the host element via el.__timeline:

Method / Property Type Description
play() () => void Start or resume playback.
stop() () => void Stop and hold at the current position.
reset() () => void Stop and reset all animated properties to initial values.
state 'idle' | 'playing' | 'stopped' Current playback state (reactive).
<div x-flow-timeline="timelineConfig" x-ref="tl"></div>

<button @click="$refs.tl.__timeline.play()">Play</button>
<button @click="$refs.tl.__timeline.stop()">Stop</button>
<button @click="$refs.tl.__timeline.reset()">Reset</button>
<span x-text="$refs.tl.__timeline.state"></span>

Full Directive Example

<div x-data="{
    timeline: {
        autoplay: false,
        loop: 2,
        lock: true,
        speed: 1,
        respectReducedMotion: true,
        steps: [
            { nodes: ['intro'], style: { opacity: '1' }, duration: 600 },
            { parallel: [
                { nodes: ['left'], position: { x: -200, y: 0 }, duration: 500 },
                { nodes: ['right'], position: { x: 200, y: 0 }, duration: 500 },
            ]},
            {
                addEdges: [{ id: 'connect', source: 'left', target: 'right' }],
                edgeTransition: 'draw',
                duration: 800,
            },
            { viewport: { zoom: 1, x: 0, y: 0 }, duration: 600 },
        ],
    },
}">
    <div x-flow-timeline="timeline" x-ref="tl"></div>
    <button @click="$refs.tl.__timeline.play()">Play Demo</button>
</div>

Demo

A combined timeline — nodes spread out, edges draw in, then particles flow:

<div x-data="flowCanvas({
    nodes: [
        { id: 'a', position: { x: 200, y: 60 }, data: { label: 'Input' } },
        { id: 'b', position: { x: 200, y: 60 }, data: { label: 'Process' } },
        { id: 'c', position: { x: 200, y: 60 }, data: { label: 'Output' } },
    ],
    edges: [],
    background: 'dots',
    controls: false,
    pannable: false,
    zoomable: false,
})" class="flow-container" style="height: 220px;"
   x-init="
       const origin = { x: 200, y: 60 };
       document.getElementById('demo-full-play').addEventListener('click', () => {
           $flow.timeline()
               .lock()
               .parallel([
                   { nodes: ['a'], position: { x: 20, y: 60 }, duration: 500, easing: 'easeOut' },
                   { nodes: ['b'], position: { x: 200, y: 60 }, duration: 500, easing: 'easeOut' },
                   { nodes: ['c'], position: { x: 380, y: 60 }, duration: 500, easing: 'easeOut' },
               ])
               .step({
                   addEdges: [{ id: 'e1', source: 'a', target: 'b' }],
                   edgeTransition: 'draw',
                   duration: 500,
               })
               .step({
                   addEdges: [{ id: 'e2', source: 'b', target: 'c' }],
                   edgeTransition: 'draw',
                   duration: 500,
               })
               .step({
                   onStart: () => {
                       $flow.sendParticle('e1', { color: '#DAA532', size: 5, duration: '1s' });
                       setTimeout(() => $flow.sendParticle('e2', { color: '#8B5CF6', size: 5, duration: '1s' }), 400);
                   },
                   duration: 1200,
               })
               .play();
       });
       document.getElementById('demo-full-reset').addEventListener('click', () => {
           removeEdges(['e1', 'e2']);
           $flow.update({ nodes: {
               a: { position: origin },
               b: { position: origin },
               c: { position: origin },
           }});
       });
   ">
    <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>