
svelte-flow
by linehaul-ai
Claude Marketplace of some of the tools we use with Claude Code
SKILL.md
name: svelte-flow description: Build node-based editors, interactive diagrams, and flow visualizations using Svelte Flow. Use when creating workflow editors, data flow diagrams, organizational charts, mindmaps, process visualizations, DAG editors, or any interactive node-graph UI. Supports custom nodes/edges, layouts (dagre, hierarchical), animations, and advanced features like proximity connect, floating edges, and contextual zoom.
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}
/>
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon
