Flow
An interactive node-and-edge canvas for flowcharts, pipelines and dependency graphs. Nodes are plain divs and each edge is one SVG path, so it stays crisp at any zoom and renders on the server; the initial layout is deterministic — every node names its column and row. From there the viewport is live: drag the background to pan, wheel or the control cluster to zoom, fit to frame everything, and drag a node to reposition it. Click a node to toggle its selection, or Tab to it and press Enter. With editable set, the canvas becomes an editor — draw edges between nodes, double-click to add, and delete the selected node — reporting each change through callbacks while the caller owns the data. Each node carries a status string beside its tone dot, so colour is never the only signal. The live version is on the Dependency graph showcase.
const nodes = [ { id: 'gateway', title: 'api-gateway', subtitle: 'Ingress', tone: 'success', status: 'Healthy', column: 0, row: 1 }, { id: 'orders', title: 'orders-api', subtitle: 'Service', tone: 'success', status: 'Healthy', column: 1, row: 0 }, { id: 'auth', title: 'auth-svc', subtitle: 'Service', tone: 'warning', status: 'Degraded', column: 1, row: 2 }, { id: 'orders-db', title: 'orders-db', subtitle: 'PostgreSQL', tone: 'success', status: 'Healthy', column: 2, row: 0, selected: true }, { id: 'events', title: 'events-queue', subtitle: 'Kafka', tone: 'neutral', status: 'Idle', column: 2, row: 2 },];
const edges = [ { from: 'gateway', to: 'orders', label: '1.2k rps', emphasis: true }, { from: 'gateway', to: 'auth' }, { from: 'orders', to: 'orders-db', emphasis: true }, { from: 'orders', to: 'events', variant: 'dashed' }, { from: 'auth', to: 'orders-db' },];
// Drag the background to pan, wheel to zoom, drag a node to move it, click to select.<Flow nodes={nodes} edges={edges} height={360} ariaLabel="Service dependency graph" className="rounded-xl border border-hairline"/>Editing
Set editable and own nodes/edges in state. Flow reports edits through onConnect, onNodeAdd, onNodeDelete, onEdgeDelete and onEdgeChange. Double-click the canvas to add a node, drag between the side handles to connect two nodes, and select a node then use its × control or the Delete key to remove it. Click a link to select it: a small toolbar appears for switching its line style (solid/dashed), arrowheads (none/end/both) and colour, or deleting it. To build your own style UI instead — say, in a side panel — read the selected edge from onEdgeSelect and write back with onEdgeChange.
function PipelineEditor() { const [nodes, setNodes] = useState(INITIAL_NODES); const [edges, setEdges] = useState(INITIAL_EDGES); const [selectedId, setSelectedId] = useState(null);
return ( <Flow nodes={nodes} edges={edges} editable selectedId={selectedId} onSelectChange={setSelectedId} onConnect={({ from, to }) => setEdges((e) => [...e, { from, to }])} onNodeAdd={(point) => setNodes((n) => [ ...n, { id: nextId(), title: 'step', column: 0, row: 0, x: point.x - 88, y: point.y - 28 }, ]) } onNodeDelete={(id) => { setNodes((n) => n.filter((node) => node.id !== id)); setEdges((e) => e.filter((edge) => edge.from !== id && edge.to !== id)); }} onEdgeDelete={(edge) => setEdges((e) => e.filter((x) => !(x.from === edge.from && x.to === edge.to))) } // The built-in toolbar sends { variant } / { arrow } / { tone } patches. onEdgeChange={(edge, patch) => setEdges((e) => e.map((x) => (x.from === edge.from && x.to === edge.to ? { ...x, ...patch } : x)), ) } /> );}Props
| Prop | Type | Default | Notes |
|---|---|---|---|
nodes | FlowNode[] | — | Boxes on the canvas, first placed by column and row. |
edges | FlowEdge[] | — | Directed connections between nodes. |
height | number | string | 460 | Viewport height; the canvas pans and zooms within it. |
ariaLabel | string | 'Flow diagram' | Accessible name for the whole diagram. |
controls | boolean | true | Show the built-in fit / zoom control cluster. |
fitOnMount | boolean | true | Frame every node inside the viewport once, on mount. |
minZoom | number | 0.4 | Lower zoom bound. |
maxZoom | number | 2 | Upper zoom bound. |
zoomStep | number | 0.2 | Per-step zoom factor for the wheel and buttons. |
selectedId | string | null | — | Controlled selection. Omit for uncontrolled. |
defaultSelectedId | string | null | — | Uncontrolled initial selection. |
onSelectChange | (id: string | null) => void | — | Fires when selection toggles. |
onNodesChange | (positions: Record<string, FlowPoint>) => void | — | Fires after a node drag or keyboard nudge ends. |
editable | boolean | false | Enable connection handles, the delete control and double-click to add. |
onConnect | (c: FlowConnection) => void | — | Fires when an edge is drawn from one node to another. |
onNodeAdd | (point: FlowPoint) => void | — | Fires on double-click of empty canvas, with canvas coordinates. |
onNodeDelete | (id: string) => void | — | Fires from the × control or the Delete key on the selected node. |
onEdgeDelete | (edge: FlowEdge) => void | — | Fires from the edge toolbar or the Delete key on a selected edge. |
onEdgeChange | (edge: FlowEdge, patch: Partial<FlowEdge>) => void | — | Fires from the edge toolbar with a variant/arrow/tone patch; enables the toolbar. |
onEdgeSelect | (edge: FlowEdge | null) => void | — | Fires when edge selection changes, for driving a custom style UI. |
columnWidth | number | 176 | Node width and initial column pitch, in px. |
columnGap | number | 72 | Initial horizontal gap between columns, in px. |
rowHeight | number | 84 | Initial vertical pitch between rows, in px. |
nodeHeight | number | 56 | Node card height, in px. |
FlowNode
| Prop | Type | Default | Notes |
|---|---|---|---|
id | string | — | Stable node id, referenced by edges. |
title | ReactNode | — | Primary label. |
subtitle | ReactNode | — | Quieter second line, e.g. a type or owner. |
icon | ReactNode | — | Leading glyph, shown in a tone-tinted tile. |
tone | BadgeTone | 'neutral' | Drives the corner status dot and the icon tile. |
status | string | — | Textual status, so the dot colour is never the only signal. |
column | number | — | Zero-based column (horizontal layer) for the initial layout. |
row | number | — | Zero-based row within the initial layout grid. |
x / y | number | — | Explicit initial position in canvas px; overrides column/row when set. |
selected | boolean | false | Selects this node on first render (uncontrolled). |
FlowEdge
| Prop | Type | Default | Notes |
|---|---|---|---|
from | string | — | Source node id. |
to | string | — | Target node id. |
label | string | — | Short mid-path label, e.g. “1.2k rps”. |
variant | 'solid' | 'dashed' | 'solid' | Line style — dashed reads as an async or indirect link. |
arrow | 'end' | 'start' | 'both' | 'none' | 'end' | Arrowhead placement; none for an undirected link, both for bidirectional. |
tone | BadgeTone | — | Colours the line and its arrow; defaults to a hairline grey. |
emphasis | boolean | false | Accent colour and a thicker line for a highlighted path. |
id | string | — | Stable key; falls back to `${from}->${to}`. |
FlowLegend
| Prop | Type | Default | Notes |
|---|---|---|---|
items | FlowLegendItem[] | — | Each names a status dot (tone) or a line style (line) in text. |