Animate & Update

AlpineFlow provides two core methods for changing node, edge, and viewport properties: update() for instant changes and animate() for smooth transitions.

update() vs animate()

Both methods accept the same AnimateTargets shape and AnimateOptions, but differ in their defaults:

Method Default Duration Use Case
$flow.update() 0 (instant) Snap properties to new values without visual transition
$flow.animate() 300 ms Smoothly interpolate properties over time
// Instant — node jumps to the new position
$flow.update({ nodes: { 'node-1': { position: { x: 300, y: 200 } } } });

// Smooth — node glides to the new position over 300ms
$flow.animate({ nodes: { 'node-1': { position: { x: 300, y: 200 } } } });

// update() with an explicit duration behaves identically to animate()
$flow.update(
  { nodes: { 'node-1': { position: { x: 300, y: 200 } } } },
  { duration: 500, easing: 'easeInOut' }
);

Click each button to see the difference — "Update" snaps instantly, "Animate" glides smoothly:

<div x-data="flowCanvas({
    nodes: [
        { id: 'u1', position: { x: 30, y: 15 }, data: { label: 'update()' } },
        { id: 'a1', position: { x: 250, y: 15 }, data: { label: 'animate()' } },
    ],
    edges: [],
    background: 'dots',
    controls: false,
    pannable: false,
    zoomable: false,
})" class="flow-container" style="height: 200px;"
   x-init="
       let uDown = false, aDown = false;
       document.getElementById('demo-update-btn').addEventListener('click', () => {
           uDown = !uDown;
           $flow.update({ nodes: { u1: { position: { x: 30, y: uDown ? 80 : 15 } } } });
       });
       document.getElementById('demo-animate-btn').addEventListener('click', () => {
           aDown = !aDown;
           $flow.animate({ nodes: { a1: { position: { x: 250, y: aDown ? 80 : 15 } } } }, { duration: 600, easing: 'easeInOut' });
       });
       document.getElementById('demo-ua-reset').addEventListener('click', () => {
           uDown = false; aDown = false;
           $flow.update({ nodes: {
               u1: { position: { x: 30, y: 15 } },
               a1: { position: { x: 250, y: 15 } },
           }});
       });
   ">
    <div x-flow-viewport>
        <template x-for="node in nodes" :key="node.id">
            <div x-flow-node="node">
                <span x-text="node.data.label"></span>
            </div>
        </template>
    </div>
</div>

AnimateTargets

The targets object has three optional keys — nodes, edges, and viewport:

interface AnimateTargets {
    nodes?: Record<string, AnimateNodeTarget>;
    edges?: Record<string, AnimateEdgeTarget>;
    viewport?: AnimateViewportTarget;
}

Node Targets (keyed by node ID)

Property Type Animated? Description
position { x?, y? } Yes Move node to position
dimensions { width?, height? } Yes Resize node
style string | Record<string, string> Yes CSS style interpolation
class string Instant CSS class replacement
data Record<string, any> Instant Merge into node data
selected boolean Instant Selection state
zIndex number Instant Z-index override

Edge Targets (keyed by edge ID)

Property Type Animated? Description
color string Yes Stroke color interpolation
strokeWidth number Yes Stroke width
label string Instant Edge label text
animated boolean Instant Dash animation toggle
class string Instant CSS class replacement

Viewport Target

Property Type Description
pan { x?, y? } Pan to position
zoom number Zoom level

AnimateOptions

interface AnimateOptions {
    duration?: number;    // ms. 0 = instant. Default: 300 (animate) / 0 (update)
    easing?: EasingName | ((t: number) => number);  // Default: 'easeInOut'
    delay?: number;       // ms before starting. Default: 0
    loop?: boolean | 'reverse';  // true = forever, 'reverse' = ping-pong
    onProgress?: (progress: number) => void;
    onComplete?: () => void;
}

Compare easing presets — all nodes move to the same target, each with a different curve:

<div x-data="flowCanvas({
    nodes: [
        { id: 'n1', position: { x: 10, y: 10 }, data: { label: 'linear' } },
        { id: 'n2', position: { x: 10, y: 60 }, data: { label: 'easeInOut' } },
        { id: 'n3', position: { x: 10, y: 110 }, data: { label: 'easeBounce' } },
        { id: 'n4', position: { x: 10, y: 160 }, data: { label: 'easeElastic' } },
    ],
    edges: [],
    background: 'dots',
    controls: false,
    pannable: false,
    zoomable: false,
})" class="flow-container" style="height: 280px;"
   x-init="
       const startX = 10, endX = 300, dur = 1500;
       let atEnd = false;
       document.getElementById('demo-easing-play').addEventListener('click', () => {
           const targetX = atEnd ? startX : endX;
           $flow.animate({ nodes: { n1: { position: { x: targetX } } } }, { duration: dur, easing: 'linear' });
           $flow.animate({ nodes: { n2: { position: { x: targetX } } } }, { duration: dur, easing: 'easeInOut' });
           $flow.animate({ nodes: { n3: { position: { x: targetX } } } }, { duration: dur, easing: 'easeBounce' });
           $flow.animate({ nodes: { n4: { position: { x: targetX } } } }, { duration: dur, easing: 'easeElastic' });
           atEnd = !atEnd;
       });
       document.getElementById('demo-easing-reset').addEventListener('click', () => {
           atEnd = false;
           $flow.update({ nodes: {
               n1: { position: { x: startX, y: 10 } },
               n2: { position: { x: startX, y: 60 } },
               n3: { position: { x: startX, y: 110 } },
               n4: { position: { x: startX, y: 160 } },
           }});
       });
   ">
    <div x-flow-viewport>
        <template x-for="node in nodes" :key="node.id">
            <div x-flow-node="node">
                <span x-text="node.data.label" style="font-family: monospace; font-size: 12px;"></span>
            </div>
        </template>
    </div>
</div>

FlowAnimationHandle

Both update() and animate() return a FlowAnimationHandle for controlling the in-flight animation:

interface FlowAnimationHandle {
    pause(): void;
    resume(): void;
    stop(): void;
    reverse(): void;
    readonly finished: Promise<void>;
}
const handle = $flow.animate({
    nodes: {
        'node-1': { position: { x: 300, y: 100 } },
        'node-2': { position: { x: 500, y: 200 }, style: { opacity: '0.8' } },
    },
    edges: {
        'edge-1': { color: '#10b981', strokeWidth: 3 },
    },
}, {
    duration: 500,
    easing: 'easeOut',
    onComplete: () => console.log('done'),
});

// Control the animation
handle.pause();
handle.resume();

Start a slow animation, then use the controls to pause, resume, reverse, or stop it:

<div x-data="flowCanvas({
    nodes: [
        { id: 'mover', position: { x: 0, y: 50 }, data: { label: 'Controlled' } },
        { id: 'target', position: { x: 450, y: 50 }, data: { label: 'Target' }, draggable: false },
    ],
    edges: [
        { id: 'e1', source: 'mover', target: 'target' },
    ],
    background: 'dots',
    fitViewOnInit: true,
    controls: false,
    pannable: false,
    zoomable: false,
})" class="flow-container" style="height: 220px;"
   x-init="
       let handle = null;
       let done = false;
       const startPos = { x: 0, y: 50 };
       const endPos = { x: 400, y: 50 };
       function go(from, to, edgeColor, edgeWidth) {
           $flow.update({ nodes: { mover: { position: from } } });
           done = false;
           handle = $flow.animate({
               nodes: { mover: { position: to } },
               edges: { e1: { color: edgeColor, strokeWidth: edgeWidth } },
           }, { duration: 3000, easing: 'linear', onComplete: () => { done = true; } });
       }
       document.getElementById('demo-handle-start').addEventListener('click', () => {
           if (handle) handle.stop();
           go(startPos, endPos, '#10b981', 3);
       });
       document.getElementById('demo-handle-pause').addEventListener('click', () => handle?.pause());
       document.getElementById('demo-handle-resume').addEventListener('click', () => handle?.resume());
       document.getElementById('demo-handle-reverse').addEventListener('click', () => {
           if (!handle) return;
           if (done) {
               go(endPos, startPos, null, null);
           } else {
               handle.reverse();
           }
       });
       document.getElementById('demo-handle-stop').addEventListener('click', () => {
           if (handle) { handle.stop(); handle = null; }
           done = false;
           $flow.update({ nodes: { mover: { position: startPos } }, edges: { e1: { color: null, strokeWidth: null } } });
       });
   ">
    <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>

Per-Element Timing Overrides

Individual targets can override the global duration and easing using _duration and _easing:

$flow.animate({
    nodes: {
        'fast-node': { position: { x: 100 }, _duration: 200 },
        'slow-node': { position: { x: 500 }, _duration: 1000 },
    },
}, { duration: 500 }); // default for targets without _duration

A target with _duration: 0 applies its changes instantly while other targets animate.

Named Animations

Register reusable animation sequences by name, then trigger them from anywhere:

// Register
$flow.registerAnimation('intro', [
    { nodes: ['a', 'b', 'c'], position: { x: 0, y: 0 }, duration: 0 },
    { nodes: ['a'], position: { x: 100, y: 50 }, duration: 500 },
    { nodes: ['b'], position: { x: 300, y: 50 }, duration: 500 },
    { nodes: ['c'], position: { x: 200, y: 200 }, duration: 500 },
]);

// Play
await $flow.playAnimation('intro');

// Unregister when no longer needed
$flow.unregisterAnimation('intro');

Named animations are also the mechanism behind the x-flow-animate directive's argument syntax:

<div x-flow-animate:intro="introSteps"></div>
<button @click="$flow.playAnimation('intro')">Play Intro</button>

x-flow-animate Directive

Trigger one-shot animations on nodes, edges, or the viewport from DOM events.

Basic Usage

<button x-flow-animate="{ nodes: ['node-1'], position: { x: 300, y: 100 }, duration: 500, easing: 'easeInOut' }">
    Move Node
</button>

Step Shape

{
    nodes: ['id'],           // target node IDs
    edges: ['id'],           // target edge IDs
    viewport: true,          // target the viewport

    // Node properties
    position: { x, y },
    dimensions: { width, height },
    style: { ... },
    class: 'name',
    data: { key: value },
    selected: true,
    zIndex: 10,

    // Edge properties
    color: '#ff0000',
    strokeWidth: 3,
    label: 'new label',
    animated: true,
    class: 'name',

    // Viewport properties
    pan: { x, y },
    zoom: 1.5,

    // Timing
    duration: 500,
    easing: 'easeInOut',
    delay: 0,
}

Sequential Steps

When an array of steps is provided, they execute sequentially:

<button x-flow-animate="[
    { nodes: ['a'], position: { x: 100, y: 0 }, duration: 300 },
    { nodes: ['b'], position: { x: 200, y: 0 }, duration: 300 },
]">
    Move A then B
</button>

Named Animations via Argument

Use :name to register a named animation that can be triggered programmatically via playAnimation():

<div x-flow-animate:intro="[
    { nodes: ['a', 'b'], position: { x: 0, y: 0 }, duration: 600 },
    { viewport: true, zoom: 1, duration: 400 },
]"></div>
$flow.playAnimation('intro');

Modifiers

Modifier Description
.click Trigger on click (default).
.mouseenter Trigger on mouse enter.
.once Play the animation only once; subsequent triggers are ignored.
.reverse Automatically reverse the animation when triggered again.
.queue Queue the animation instead of cancelling the previous one.

Examples

Mouseenter with auto-reverse -- nodes move to the target position on hover; triggering again returns them:

<div x-flow-animate.mouseenter.reverse="{
    nodes: ['preview'],
    position: { x: 0, y: -50 },
    style: { opacity: '1' },
    duration: 300,
}">
    Hover to preview
</div>

One-shot intro:

<button x-flow-animate.click.once="[
    { nodes: ['step-1'], style: { opacity: '1' }, duration: 400 },
    { nodes: ['step-2'], style: { opacity: '1' }, duration: 400 },
    { viewport: true, zoom: 1, pan: { x: 0, y: 0 }, duration: 600 },
]">
    Play Intro
</button>

Demo

Hover over each node to see it react — the directive handles the animation and auto-reverses on mouse leave:

INTERACTIVE
<div x-data="flowCanvas({
    nodes: [
        { id: 'a', position: { x: 0, y: 60 }, data: { label: 'Hover me' } },
        { id: 'b', position: { x: 200, y: 0 }, data: { label: 'Or me' } },
        { id: 'c', position: { x: 400, y: 60 }, data: { label: 'Or me' } },
    ],
    edges: [
        { id: 'e1', source: 'a', target: 'b' },
        { id: 'e2', source: 'b', target: 'c' },
    ],
    background: 'dots',
    fitViewOnInit: true,
    controls: false,
    pannable: false,
    zoomable: false,
})" class="flow-container" style="height: 230px;">
    <div x-flow-viewport>
        <template x-for="node in nodes" :key="node.id">
            <div x-flow-node="node"
                 x-flow-animate.mouseenter.reverse="{
                     nodes: [node.id],
                     style: { transform: 'scale(1.08) translateY(-10px)' },
                     duration: 250,
                     easing: 'easeOut',
                 }">
                <div x-flow-handle:target></div>
                <span x-text="node.data.label"></span>
                <div x-flow-handle:source></div>
            </div>
        </template>
    </div>
</div>