Core Concepts
AlpineFlow builds on Alpine.js to turn declarative HTML into interactive flow diagrams. Understanding these six concepts will help you work with every part of the library.
The flow container
Every flow starts with a container element that has two things: the flow-container CSS class and an x-data="flowCanvas({...})" attribute.
<div x-data="flowCanvas({ nodes: [...], edges: [...] })" class="flow-container" style="height: 500px;">
<!-- viewport, nodes, etc. -->
</div>
Both are required for different reasons:
x-data="flowCanvas({...})"registers the Alpine data component that manages all flow state — nodes, edges, viewport position, selection, history, and more. This is where you pass your initial configuration.class="flow-container"applies the structural CSS that sets up positioning, overflow clipping, and the coordinate system the viewport operates within. Without it, nodes won't position correctly and pan/zoom won't work.
The container must have an explicit height (via style, a CSS class, or a parent layout) since flow diagrams don't have intrinsic dimensions.
Directives
AlpineFlow extends Alpine with x-flow-* directives that attach flow behavior to HTML elements. The naming convention follows a consistent pattern:
x-flow-{feature}— the base directive, e.g.x-flow-viewport,x-flow-node,x-flow-handle- Arguments via colon — pass a role or type after a colon, e.g.
x-flow-handle:source(a source handle) orx-flow-handle:target(a target handle) - Modifiers via dot — append behavior modifiers with dots, e.g.
x-flow-handle:source.right(a source handle positioned on the right edge) orx-flow-collapse.group(collapse with group semantics)
This mirrors Alpine's own syntax (x-on:click.prevent, x-bind:class), so if you know Alpine, the pattern is familiar.
Some commonly used directives:
| Directive | Purpose |
|---|---|
x-flow-viewport |
Wraps the pannable/zoomable layer |
x-flow-node="node" |
Makes an element a positioned, draggable node |
x-flow-handle:source |
Adds a connection handle for outgoing edges |
x-flow-handle:target |
Adds a connection handle for incoming edges |
x-flow-drag-handle |
Restricts node dragging to a specific child element |
x-flow-action:fitView |
Binds a button click to a flow action |
See Nodes for the full reference.
The viewport
The x-flow-viewport directive creates the pannable and zoomable layer. All nodes must be placed inside it:
<div x-data="flowCanvas({...})" class="flow-container">
<div x-flow-viewport>
<!-- nodes go here -->
</div>
</div>
The viewport translates mouse drags into panning and scroll wheel events into zooming. It applies a CSS transform to move and scale all child content together. Elements placed outside the viewport (like toolbars or overlays) stay fixed relative to the container.
Edges are not placed inside the viewport manually. AlpineFlow renders an SVG edge layer automatically based on the edges array — you never write edge markup.
Reactive data
The nodes and edges arrays you pass to flowCanvas() become reactive Alpine data. Mutating them updates the UI immediately:
// Adding a node at runtime
$flow.addNodes({ id: 'c', position: { x: 100, y: 200 }, data: { label: 'New' } });
// Removing an edge
edges = edges.filter(e => e.id !== 'e1');
// Updating a node's data
nodes.find(n => n.id === 'a').data.label = 'Updated';
This works because Alpine's reactivity system tracks property access and triggers re-renders when values change. There is no separate "setState" or "dispatch" step — direct mutation is the intended pattern.
Key points:
- Nodes require
id,position: { x, y }, anddata(an object for your custom properties) - Edges require
id,source(node ID), andtarget(node ID) - Edges are rendered automatically from the array — no edge templates or markup needed
- Adding, removing, or modifying items in either array triggers a UI update
The $flow magic
AlpineFlow registers a $flow magic property (available via Alpine's magic system) that gives you programmatic access to the canvas from any expression inside the flow container:
<button @click="$flow.fitView()">Fit View</button>
<button @click="$flow.zoomTo(1.5)">Zoom to 150%</button>
<button @click="$flow.addNodes({ id: 'x', position: { x: 0, y: 0 }, data: { label: 'Added' } })">
Add Node
</button>
Common $flow methods include:
| Method | Description |
|---|---|
fitView(options?) |
Pan and zoom to fit all (or selected) nodes in view |
addNodes(node | nodes[]) |
Add one or more nodes to the canvas |
removeNodes(ids[]) |
Remove nodes (and their connected edges) |
setViewport(viewport, options?) |
Set pan and zoom level |
animate(targets, options?) |
Smoothly transition node, edge, or viewport properties |
getNode(id) |
Retrieve a node by ID |
toObject() |
Export the full flow state as a serializable object |
See $flow Magic for the complete API.
Scope rules
Alpine evaluates attributes in the scope of the nearest x-data ancestor. This creates an important subtlety: directives placed on the same element as x-data evaluate in that element's own scope, not a parent's.
This means that on the flowCanvas element itself, you cannot reference variables from a parent x-data:
<!-- This WON'T work — parentVar is not in scope on the flowCanvas element -->
<div x-data="{ parentVar: 'hello' }">
<div x-data="flowCanvas({...})" :class="parentVar">
...
</div>
</div>
If you need parent data accessible inside the flow container (common in WireFlow/Livewire setups), use Object.assign($data, ...) to merge it into the flow's scope, or restructure so the parent data lives on the same element:
<!-- Option 1: merge via Object.assign in x-init -->
<div
x-data="flowCanvas({...})"
x-init="Object.assign($data, { parentVar: 'hello' })"
class="flow-container"
>
...
</div>
This is particularly relevant when using WireFlow, where Livewire's wire:model bindings need to coexist with the flowCanvas data scope.
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 20, y: 30 }, data: { label: 'Source' } },
{ id: 'b', position: { x: 250, y: 30 }, data: { label: 'Target' } },
],
edges: [
{ id: 'e1', source: 'a', target: 'b' },
],
background: 'dots',
controls: false,
pannable: false,
zoomable: false,
})" class="flow-container" style="height: 180px;">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target.left></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source.right></div>
</div>
</template>
</div>
</div>