Flow
Native left-to-right workflow canvas — pan, zoom, multi-output nodes, drag-to-connect edges, snap grid, toolbar and minimap. Zero third-party graph libraries.
Basic Usage
Pass
nodes
and edges
. Positions are optional — missing ones are auto-laid out left to right.
Empty flow
Add nodes to start building your workflow.
Drag nodes · scroll to zoom · drag background to pan · drag a + / right port onto another node to connect · Delete to remove
<neura::flow :nodes="$nodes" :edges="$edges" height="380px" />
Branching workflow
Multiple outputs via
data.outputs
. Edges use sourceHandle
to pick a port.
Empty flow
Add nodes to start building your workflow.
$nodes = [
['id' => 'w2', 'data' => [
'label' => 'Review',
'outputs' => [
['id' => 'ok', 'label' => 'Approve'],
['id' => 'ko', 'label' => 'Reject'],
],
]],
];
$edges = [
['source' => 'w2', 'target' => 'w3', 'sourceHandle' => 'ok', 'animated' => true],
['source' => 'w2', 'target' => 'w4', 'sourceHandle' => 'ko'],
];
<neura::flow :nodes="$nodes" :edges="$edges" height="420px" />
Empty canvas
Empty flow
Add nodes to start building your workflow.
<neura::flow :nodes="[]" :edges="[]" height="280px" :showMinimap="false" />
Snap, minimap & edges
Tune interaction and chrome with props:
<neura::flow
:nodes="$nodes"
:edges="$edges"
height="500px"
:snapToGrid="true"
:gridSize="16"
:showMinimap="true"
:showToolbar="true"
edgeType="smoothstep"
:defaultEdgeAnimated="false"
backgroundClasses="grid"
toolbarClasses="bottom left"
:minZoom="0.2"
:maxZoom="2.5"
:zoomOnWheelScroll="true"
:panOnDrag="true"
:nodesConnectable="true"
:fitViewOnInit="true"
/>
Data structure
// Node
[
'id' => 'unique-id',
'type' => 'step',
'position' => ['x' => 120, 'y' => 80], // optional
'data' => [
'label' => 'Title',
'description' => 'Subtitle',
'icon' => '▶',
'color' => '#3b82f6', // accent bar
'status' => 'running',
'outputs' => [ // multi-output ports (right)
['id' => 'success', 'label' => 'Success'],
['id' => 'error', 'label' => 'Error'],
],
],
]
// Edge
[
'id' => 'e1', // optional
'source' => 'node-a',
'target' => 'node-b',
'sourceHandle' => 'success', // default: out
'targetHandle' => 'in', // default: in
'type' => 'bezier', // or smoothstep
'animated' => true,
'sourceOffset' => -12, // shift attachment up/down (px)
'targetOffset' => 18,
]
// Self-loop (same node)
['source' => 'node-a', 'target' => 'node-a']
Select an edge → drag an endpoint up/down along the node to reposition (live). Drag onto another node to reconnect.
Or call
flow.updateEdge(id, { target: '3', targetOffset: 20 })
.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| nodes | array | [] | Initial nodes |
| edges | array | [] | Connections |
| height | string | 500px | Canvas height |
| direction | string | lr | Flow direction (left → right) |
| edgeType | string | bezier | bezier | smoothstep (loops always use an orthogonal U-turn) |
| defaultEdgeAnimated | boolean | false | Animate edges created by drag-connect / addEdge |
| snapToGrid | boolean | true | Snap node drag to grid |
| gridSize | number | 16 | Snap grid size |
| showMinimap | boolean | true | Overview map |
| showToolbar | boolean | true | Zoom / fit / delete controls |
| backgroundClasses | string | dots | dots | grid | none |
| minZoom / maxZoom | number | 0.25 / 2.5 | Zoom limits |
| zoomOnWheelScroll | boolean | true | Wheel zooms toward cursor |
| panOnDrag | boolean | true | Drag empty canvas to pan |
| nodesDraggable | boolean | true | Allow moving nodes |
| nodesConnectable | boolean | true | Allow drawing edges |
| fitViewOnInit | boolean | true | Fit graph on load (alias: autoCenter) |
Events
Events bubble from the flow root. Listen with Alpine / Livewire:
| Event | Detail |
|---|---|
| flow-init | nodes, edges, viewport |
| flow-change | Full toObject()
snapshot |
| flow-connect | New edge |
| flow-edge-update | Edge reconnected / repositioned |
| flow-node-select | Selected node ids |
| flow-nodes-deleted | Deleted ids |
<div @flow-change="console.log($event.detail)">
<neura::flow :nodes="$nodes" :edges="$edges" />
</div>
API
Alpine component:
neuraFlow
(alias flowEditor
).
<div x-data="{ flow: null }">
<div
x-data="neuraFlow({ nodes: [], edges: [] })"
x-init="flow = $data"
style="height: 420px"
class="flow"
></div>
<button @click="flow.addNode({ data: { label: 'New step' } })">Add</button>
<button @click="flow.addEdge({ source: '1', target: '2', animated: true })">Connect (animated)</button>
<button @click="flow.fitView()">Fit</button>
<button @click="console.log(flow.toObject())">Export</button>
</div>
Methods:
addNode
, removeNodes
, addEdge
, updateEdge
, removeEdges
, fitView
, setViewport
, zoomIn
, zoomOut
, toObject
, getNode
, selectNodes
, deleteSelection
.