svelte-flow
Svelte Flow
Expert guide for building node-based UIs with Svelte Flow (@xyflow/svelte).
Installation
npm install @xyflow/svelte
Core Setup
<script lang="ts">
import { SvelteFlow, Background } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
// Use $state.raw for nodes and edges (Svelte 5)
let nodes = $state.raw([
{ id: '1', data: { label: 'Node 1' }, position: { x: 0, y: 0 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 0, y: 100 } }
]);
let edges = $state.raw([
{ id: 'e1-2', source: '1', target: '2' }
]);
</script>
<SvelteFlow bind:nodes bind:edges fitView>
<Background />
</SvelteFlow>
Key Concepts
Nodes
Each node requires:
id: Unique identifierposition:{ x: number, y: number }data: Object with custom properties (typically includeslabel)- Optional:
type,style,hidden,width,height,sourcePosition,targetPosition
const node = {
id: '1',
type: 'custom',
position: { x: 100, y: 100 },
data: { label: 'My Node', color: '#ff0000' },
style: 'background: #f0f0f0',
width: 150,
height: 100
};
Edges
Each edge requires:
id: Unique identifiersource: Source node IDtarget: Target node ID- Optional:
type,label,animated,markerEnd,markerStart,style,hidden
const edge = {
id: 'e1-2',
source: '1',
target: '2',
type: 'smoothstep',
label: 'connects to',
animated: true,
markerEnd: { type: MarkerType.ArrowClosed }
};
Built-in Types
Edge Types:
default: Straight linesmoothstep: Smooth 90-degree turnsstep: Hard 90-degree turnsstraight: Alias for defaultbezier: Curved bezier line
Node Types:
default: Basic rectangle nodeinput: Node with only source handlesoutput: Node with only target handles
Custom Nodes
<!-- CustomNode.svelte -->
<script lang="ts" module>
import type { Node, NodeProps } from '@xyflow/svelte';
export type CustomNodeType = Node<{
label: string;
color?: string;
}>;
</script>
<script lang="ts">
import { Handle, Position } from '@xyflow/svelte';
let { data }: NodeProps<CustomNodeType> = $props();
</script>
<div class="custom-node" style:background={data.color}>
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />
</div>
<style>
.custom-node {
padding: 10px;
border-radius: 5px;
border: 1px solid #ddd;
}
</style>
Register custom nodes:
<script lang="ts">
import CustomNode from './CustomNode.svelte';
const nodeTypes = {
custom: CustomNode
};
let nodes = $state.raw([
{ id: '1', type: 'custom', data: { label: 'Custom', color: '#ff7000' }, position: { x: 0, y: 0 } }
]);
</script>
<SvelteFlow bind:nodes {nodeTypes} bind:edges fitView />
Custom Edges
<!-- CustomEdge.svelte -->
<script lang="ts">
import { BaseEdge, EdgeLabel, getStraightPath, type EdgeProps } from '@xyflow/svelte';
let { id, sourceX, sourceY, targetX, targetY, label }: EdgeProps = $props();
let [edgePath, labelX, labelY] = $derived(
getStraightPath({ sourceX, sourceY, targetX, targetY })
);
</script>
<BaseEdge {id} path={edgePath} />
{#if label}
<EdgeLabel x={labelX} y={labelY}>
<div class="edge-label">{label}</div>
</EdgeLabel>
{/if}
Path helpers available:
getStraightPath()getBezierPath()getSmoothStepPath()
Register custom edges:
<script lang="ts">
const edgeTypes = {
custom: CustomEdge
};
</script>
<SvelteFlow bind:nodes bind:edges {edgeTypes} fitView />
Updating State
Critical: Nodes and edges are immutable. Create new objects to trigger updates.
<script lang="ts">
// ❌ Won't work - direct mutation
nodes[0].position.x = 100;
// ✅ Works - create new array
nodes = nodes.map((node) => {
if (node.id === '1') {
return { ...node, position: { ...node.position, x: 100 } };
}
return node;
});
// ✅ Also works - using helper from useSvelteFlow
import { useSvelteFlow } from '@xyflow/svelte';
const { updateNode } = useSvelteFlow();
updateNode('1', (node) => ({
...node,
position: { ...node.position, x: 100 }
}));
</script>
Event Handling
Common event handlers:
<SvelteFlow
bind:nodes
bind:edges
onnodeclick={(event) => console.log('node clicked', event.targetNode)}
onnodedrag={(event) => console.log('node dragging', event.targetNode)}
onnodedragstop={(event) => console.log('drag ended', event.targetNode)}
onedgeclick={(event) => console.log('edge clicked', event.edge)}
onconnect={(params) => {
edges = [...edges, { id: `e${params.source}-${params.target}`, ...params }];
}}
/>
Available events:
onnodeclick,onnodedoubleclickonnodedrag,onnodedragstart,onnodedragstoponedgeclick,onedgedoubleclickonconnect,onconnectstart,onconnectendonbeforedelete(async, can prevent deletion)oninit,onmove,onmovestart,onmoveend
Layout Algorithms
Dagre (Hierarchical)
import dagre from '@dagrejs/dagre';
import { Position } from '@xyflow/svelte';
function getLayoutedElements(nodes: Node[], edges: Edge[], direction = 'TB') {
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction });
const nodeWidth = 172;
const nodeHeight = 36;
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
return {
...node,
targetPosition: isHorizontal ? Position.Left : Position.Top,
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
position: {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
},
};
});
return { nodes: layoutedNodes, edges };
}
Advanced Features
Floating Edges
Edges that dynamically connect to the closest point on node boundaries:
<!-- FloatingEdge.svelte -->
<script lang="ts">
import { BaseEdge, getBezierPath, type EdgeProps } from '@xyflow/svelte';
import { getEdgeParams } from './utils';
let { id, source, target, markerEnd }: EdgeProps = $props();
const { sx, sy, tx, ty, sourcePos, targetPos } = $derived(
getEdgeParams(source, target)
);
let [edgePath] = $derived(
getBezierPath({
sourceX: sx,
sourceY: sy,
sourcePosition: sourcePos,
targetX: tx,
targetY: ty,
targetPosition: targetPos,
})
);
</script>
<BaseEdge {id} path={edgePath} {markerEnd} />
Register with ConnectionMode.Loose:
<script lang="ts">
import { ConnectionMode } from '@xyflow/svelte';
</script>
<SvelteFlow
bind:nodes
bind:edges
edgeTypes={{ floating: FloatingEdge }}
connectionMode={ConnectionMode.Loose}
fitView
/>
Proximity Connect
Auto-connect nodes when dragged close together:
<script lang="ts">
const MIN_DISTANCE = 150;
function getClosestEdge(node: Node, nodes: Node[]) {
const closestNode = nodes.reduce(
(res, n) => {
if (n.id !== node.id) {
const dx = n.position.x - node.position.x;
const dy = n.position.y - node.position.y;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < res.distance && d < MIN_DISTANCE) {
res.distance = d;
res.node = n;
}
}
return res;
},
{ distance: Number.MAX_VALUE, node: null }
);
if (!closestNode.node) return null;
const closeNodeIsSource = closestNode.node.position.x < node.position.x;
return {
id: closeNodeIsSource
? `${node.id}-${closestNode.node.id}`
: `${closestNode.node.id}-${node.id}`,
source: closeNodeIsSource ? closestNode.node.id : node.id,
target: closeNodeIsSource ? node.id : closestNode.node.id,
class: 'temp',
};
}
function onNodeDrag({ targetNode: node }) {
const closestEdge = getClosestEdge(node, nodes);
edges = edges.filter(e => e.class !== 'temp');
if (closestEdge && !edges.some(e => e.source === closestEdge.source && e.target === closestEdge.target)) {
edges = [...edges, closestEdge];
}
}
function onNodeDragStop() {
edges = edges.map((edge) =>
edge.class === 'temp' ? { ...edge, class: '' } : edge
);
}
</script>
<SvelteFlow
bind:nodes
bind:edges
onnodedrag={onNodeDrag}
onnodedragstop={onNodeDragStop}
fitView
/>
Edge Reconnection
Allow users to drag edge endpoints to different nodes:
<!-- ReconnectableEdge.svelte -->
<script lang="ts">
import {
BaseEdge,
EdgeReconnectAnchor,
getBezierPath,
type EdgeProps
} from '@xyflow/svelte';
let { sourceX, sourceY, targetX, targetY, selected, ...props }: EdgeProps = $props();
const [edgePath] = $derived(
getBezierPath({ sourceX, sourceY, targetX, targetY })
);
let reconnecting = $state(false);
</script>
{#if !reconnecting}
<BaseEdge path={edgePath} {...props} />
{/if}
{#if selected}
<EdgeReconnectAnchor
bind:reconnecting
type="source"
position={{ x: sourceX, y: sourceY }}
/>
<EdgeReconnectAnchor
bind:reconnecting
type="target"
position={{ x: targetX, y: targetY }}
/>
{/if}
Delete Node and Reconnect Edges
<script lang="ts">
import { getIncomers, getOutgoers, getConnectedEdges, type OnBeforeDelete } from '@xyflow/svelte';
const onbeforedelete: OnBeforeDelete = async ({ nodes: deletedNodes }) => {
let remainingNodes = [...nodes];
edges = deletedNodes.reduce((acc, node) => {
const incomers = getIncomers(node, remainingNodes, acc);
const outgoers = getOutgoers(node, remainingNodes, acc);
const connectedEdges = getConnectedEdges([node], acc);
const remainingEdges = acc.filter((edge) => !connectedEdges.includes(edge));
const createdEdges = incomers.flatMap(({ id: source }) =>
outgoers.map(({ id: target }) => ({
id: `${source}->${target}`,
source,
target,
}))
);
remainingNodes = remainingNodes.filter((rn) => rn.id !== node.id);
return [...remainingEdges, ...createdEdges];
}, edges);
nodes = remainingNodes;
return true;
};
</script>
<SvelteFlow bind:nodes bind:edges {onbeforedelete} fitView />
Components
Controls
<script lang="ts">
import { SvelteFlow, Controls } from '@xyflow/svelte';
</script>
<SvelteFlow bind:nodes bind:edges fitView>
<Controls showLock={false} showFitView={true} showZoom={true} />
</SvelteFlow>
MiniMap
<script lang="ts">
import { SvelteFlow, MiniMap } from '@xyflow/svelte';
</script>
<SvelteFlow bind:nodes bind:edges fitView>
<MiniMap />
</SvelteFlow>
Background
<script lang="ts">
import { SvelteFlow, Background, BackgroundVariant } from '@xyflow/svelte';
</script>
<SvelteFlow bind:nodes bind:edges fitView>
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
</SvelteFlow>
Variants: Dots, Lines, Cross
Panel (Custom Controls)
<script lang="ts">
import { SvelteFlow, Panel } from '@xyflow/svelte';
</script>
<SvelteFlow bind:nodes bind:edges fitView>
<Panel position="top-left">
<button>Custom Button</button>
</Panel>
</SvelteFlow>
Positions: top-left, top-center, top-right, bottom-left, bottom-center, bottom-right
Utility Functions
import {
getConnectedEdges,
getIncomers,
getOutgoers,
addEdge,
applyEdgeChanges,
applyNodeChanges
} from '@xyflow/svelte';
// Get edges connected to nodes
const connectedEdges = getConnectedEdges(nodes, edges);
// Get nodes that connect TO a node
const incomers = getIncomers(targetNode, nodes, edges);
// Get nodes that a node connects TO
const outgoers = getOutgoers(sourceNode, nodes, edges);
// Add edge with validation
edges = addEdge(connection, edges);
Store Management (Alternative)
For centralized state:
// store.svelte.js
let nodes = $state.raw([...]);
let edges = $state.raw([...]);
export const getNodes = () => nodes;
export const getEdges = () => edges;
export const setNodes = (newNodes) => nodes = newNodes;
export const setEdges = (newEdges) => edges = newEdges;
<!-- Component.svelte -->
<script>
import { getNodes, getEdges, setNodes, setEdges } from './store.svelte.js';
</script>
<SvelteFlow bind:nodes={getNodes, setNodes} bind:edges={getEdges, setEdges} />
Export to Image
<script lang="ts">
import { useSvelteFlow } from '@xyflow/svelte';
const { toObject, getViewport } = useSvelteFlow();
async function downloadImage() {
const { toPng } = await import('html-to-image');
const dataUrl = await toPng(document.querySelector('.svelte-flow'));
const a = document.createElement('a');
a.setAttribute('download', 'flow.png');
a.setAttribute('href', dataUrl);
a.click();
}
</script>
Performance Tips
- Use
$state.rawfor nodes and edges (prevents deep reactivity) - For large graphs (1000+ nodes), set
elevateNodesOnSelect={false}andelevateEdgesOnSelect={false} - Use
minZoomandmaxZoomto limit zoom levels - Consider virtualization for extremely large graphs
Common Props
<SvelteFlow
bind:nodes
bind:edges
nodeTypes={customNodeTypes}
edgeTypes={customEdgeTypes}
defaultEdgeOptions={{ animated: true, type: 'smoothstep' }}
connectionLineType={ConnectionLineType.Straight}
connectionMode={ConnectionMode.Loose}
fitView
minZoom={0.1}
maxZoom={2}
snapToGrid={true}
snapGrid={[15, 15]}
elevateNodesOnSelect={true}
elevateEdgesOnSelect={true}
deleteKeyCode="Delete"
selectionKeyCode="Shift"
multiSelectionKeyCode="Meta"
panOnScroll={true}
zoomOnScroll={true}
zoomOnDoubleClick={true}
zoomOnPinch={true}
panOnDrag={true}
/>
More from linehaul-ai/linehaulai-claude-marketplace
geospatial-postgis-patterns
Implement geofences, spatial queries, real-time tracking, and mapping features in laneweaverTMS using PostGIS and PGRouting. Use when building location-based features, distance calculations, ETA predictions, or fleet visualization.
83quickbooks-online-api
Expert guide for QuickBooks Online API integration covering authentication, CRUD operations, batch processing, and best practices for invoicing, payments, and customer management.
61rbac-authorization-patterns
Provide patterns for implementing Role-Based Access Control and multi-tenant authorization in laneweaverTMS. Use when implementing user roles, permissions, tenant isolation, Echo authorization middleware, RLS policies for multi-tenant access, or JWT claims structure for freight brokerage applications.
61slack-block-kit
Build Slack Block Kit UIs for messages, modals, and Home tabs. Use when creating Slack notifications, interactive forms, bot responses, app dashboards, or any Slack UI. Covers blocks (Section, Actions, Input, Header), elements (Buttons, Selects, Date pickers), composition objects, and the slack-block-builder library.
44testcontainers-go
Use this skill when writing Go integration tests with Docker containers, using testcontainers-go modules (postgres, redis, kafka, etc.), setting up container-based test infrastructure, or configuring container networking and wait strategies. Covers 62+ pre-configured modules, cleanup patterns, and multi-container setups.
34slack-web-api
Comprehensive guidance for Slack Web API operations with the Go SDK. Use when sending messages, posting to channels, creating or managing channels, retrieving user information, uploading files, composing Block Kit messages, or performing any synchronous Slack API operations.
30