Flow
Flow tracks dragging, selection, and port hit-testing while you render the nodes, ports, and edges. Wrap it in Viewport if you want panning and zoom on the whole canvas.
- Flow handles: mouse/touch listeners, marquee selection, port hit-testing, group dragging, and a default edge renderer.
- You handle: node layout and visuals, unique port IDs, connection rules via
validate-edge, and any side effects when nodes/edges change. Node,Edge, andPortare generic.
Edit node text, drag ports to connect, Shift+drag to box-select, and press Backspace to delete the current selection.
Nodes state
Edges state
Usage
<script lang="ts">
import { Flow, type FlowApi } from '@obelus/trioxide';
type Port = { id: string; side: 'in' | 'out' };
type Node = { title: string; ports: Port[]; position: { x: number; y: number } };
type Edge = { from: string; to: string };
let nodes: Node[] = $state([
{
title: 'Input',
ports: [{ id: 'input', side: 'out' }],
position: { x: 40, y: 40 }
},
{
title: 'Output',
ports: [{ id: 'output', side: 'in' }],
position: { x: 280, y: 200 }
}
]);
let edges: Edge[] = $state([]);
let api: FlowApi<Node, Edge, Port> = $state()!;
const validateEdge = ([fromNode, fromPort], [toNode, toPort]) => {
if (fromNode === toNode) return;
if (fromPort.side === toPort.side) return;
return { from: fromPort.id, to: toPort.id };
};
</script>
<Flow
bind:nodes
bind:edges
bind:api
validate-edge={validateEdge}
edge-type="smooth"
default-edge-path-props={(_, isSelected) => ({
stroke: isSelected ? 'gray' : 'blue',
'stroke-linecap': 'round',
'stroke-width': 3
})}
>
{#snippet Node({ node, dragBindings, portBindings, nodeBindings })}
<article class="" {...dragBindings} {...nodeBindings}>
<div class="ports">
{#each node.ports as port}
<button class="" {...portBindings(port)} aria-label={`Connect ${port.id}`}> </button>
{/each}
</div>
<span class="text-sm">{node.title}</span>
</article>
{/snippet}
</Flow> nodes and edges are mutated in place (positions update on drag; edges are pushed and spliced), so keep them in a $state store or another mutable data structure.
Props
nodes(bind): array of nodes withpositionandports; portids must be unique across the entire graph.edges(bind):{ from: string; to: string }[]array; Flow mutates it when connections are created or removed.validate-edge(from, to): required; return an edge object to accept the connection orundefinedto reject (self-links, direction checks, etc.).Node: required snippet receiving{ node, dragBindings, portBindings, nodeBindings }; render your node and spread the bindings.'edge-type':'smooth' | 'step'(default'smooth') for the built-in edge path generator.default-edge-path-props(edge, isSelected): optional attributes merged into the default<path>(stroke color, width, dash).Edge: optional snippet(edge, fromAnchor, toAnchor, bindings); usebindings.isSelectedandbindings.edgeHandlersto keep selection working.GhostEdge: optional snippet(fromAnchor, cursorBounds)to render the live preview while dragging from a port.box-selection-props: attributes forwarded to the selection box element (e.g., custom border/fill).bind:api: optionalFlowApiwith selection sets and removal helpers for keyboard shortcuts or custom tooling.readonly: boolean; when true, disables dragging, selection, and edge creation.- Rest props are forwarded to the root
<div>(e.g., classes,tabindex, aria labels).
Node bindings
dragBindings: spread onto the node (or a child) to make it the drag handle.portBindings(port): spread onto each port element so Flow can register hit areas and finish/confirm connections.nodeBindings: spread onto the node container to register its DOM rect for box selection and collision checks.node: your original node object; mutatenode.positionyourself if you need external layout logic.
Edge rendering
- The default renderer draws between the centers of the two port bounding boxes using the chosen
edge-type. - Provide
Edgeto draw custom paths, labels, or hit areas; callbindings.edgeHandlerson the clickable layer to enable selection. - Provide
GhostEdgeto customize the edge shown while the user is dragging from a port. - Use
defaultEdgePathPropsto tweak stroke color/width without replacing the renderer.
API and behavior
api.selectedNodes/api.selectedEdges: liveSets describing the current selection.api.insertNodeAt(node, clientX, clientY): place a node at the given client coordinates and append it tonodes.api.removeNode(node)removes the node and any incident edges;api.removeEdge(edge)removes a single edge.api.activePortreports the port currently being dragged from;api.selectingtells you when the marquee is active.- Hold
Shiftand drag on the canvas to draw a selection box; click edges to select them (holdShiftfor multi-select); drag selected nodes as a group. - Pointer listeners are attached to
windowso drags continue off-canvas