Svelte Flow (@xyflow/svelte) for SvelteKit
Interactive node-based UIs — flow editors, graph/diagram canvases, workflow & pipeline builders — for
the rask SvelteKit 2 + Svelte 5 frontend. This skill targets Svelte Flow 1.0 (the Svelte 5
rewrite). The library API is small; the hard part is Svelte 5 reactivity — and that's exactly where
generated code goes wrong.
Package: @xyflow/svelte (install with bun add @xyflow/svelte). This is not React Flow
(@xyflow/react) — the APIs look similar but the components, props (lowercase events), and
reactivity model differ. Never copy React Flow code into a Svelte file.
When to use
- Building or editing a node-and-edge canvas: flow/workflow editors, pipeline/DAG builders,
mind-maps, org charts, state-machine or RAG/agent graph visualizers, whiteboards.
- Anything needing drag-to-connect handles, custom node/edge components, a minimap, pan/zoom
viewport, sub-flows (grouping), or programmatic graph manipulation.
Not for: static, non-interactive diagrams that belong inline (use Mermaid, or the
architecture-diagram skill) · data charts (use LayerChart) ·
React (@xyflow/react).
The five rules (read before writing any Svelte Flow)
These prevent ~every common bug. Details + the error each one causes are in
references/troubleshooting-and-migration.md.
- State is
$state.raw and immutable. Declare let nodes = $state.raw([...]) — never $state(...)
(deep reactivity on every node/handle tanks performance). To change a node, create a new object
and reassign the array (nodes = nodes.map(...)) or use updateNode / updateNodeData. Mutating
nodes[0].position.x = … in place does nothing.
- Bind, don't pass.
<SvelteFlow bind:nodes bind:edges />. Plain {nodes} won't write
drag/select/connect changes back to your arrays. State in another module → use
function bindings.
- Import the stylesheet, once.
import '@xyflow/svelte/dist/style.css'; in your entry/layout.
Missing it = invisible edges, broken handles, and a warning. (Tailwind: import it layer(base).)
- The wrapper needs width and height.
<SvelteFlow /> fills its parent; a zero-height <div>
renders an empty canvas. Wrap it in an element with explicit or inherited dimensions.
- Hooks run inside the flow and return
.current. useNodes(), useSvelteFlow(), etc. only work
in a component rendered inside <SvelteFlow> or <SvelteFlowProvider>. Read reactive values via
.current. Define nodeTypes / edgeTypes once at module scope (not inline) and make each key
match the node/edge type string exactly.
Quick start
<script lang="ts">
import { SvelteFlow, Background, Controls, MiniMap, BackgroundVariant } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
import type { Node, Edge } from '@xyflow/svelte';
let nodes = $state.raw<Node[]>([
{ id: 'n1', type: 'input', position: { x: 0, y: 0 }, data: { label: 'Start' } },
{ id: 'n2', position: { x: 0, y: 120 }, data: { label: 'Next' } },
]);
let edges = $state.raw<Edge[]>([
{ id: 'n1-n2', source: 'n1', target: 'n2' },
]);
</script>
<div style:width="100%" style:height="100vh">
<SvelteFlow bind:nodes bind:edges fitView colorMode="system">
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
<Controls />
<MiniMap />
</SvelteFlow>
</div>
A node needs id + position + data; an edge needs id + source + target. fitView frames the
graph on load. Scaffold the fuller version from assets/Flow.svelte.
Golden path — pick the approach
| You need… |
Use |
Where |
| A starting flow component |
golden-path template |
assets/Flow.svelte |
| Custom node content (inputs, charts, multiple handles) |
a Svelte component + nodeTypes |
assets/TextUpdaterNode.svelte, nodes ref |
| A custom edge (label, delete button, custom path) |
BaseEdge + a path util + EdgeLabel |
assets/ButtonEdge.svelte, edges ref |
| Read/mutate the graph imperatively |
useSvelteFlow() + hooks (.current) |
hooks ref |
| Graph/edge state in a shared module |
flow.svelte.ts + function bindings |
assets/flow.svelte.ts |
| Auto-arrange nodes |
external lib (dagre/elk) → reassign nodes |
assets/dagre-layout.ts, layout ref |
| Persist/measure handles on the server |
explicit width/height/handles |
SSR in svelte-integration ref |
Custom nodes & edges (the 80%)
Custom nodes/edges are plain Svelte components that receive typed props. Register them once and refer to
them by the type key.
<!-- TextUpdaterNode.svelte — a custom node -->
<script lang="ts">
import { Handle, Position, useSvelteFlow, type NodeProps } from '@xyflow/svelte';
let { id, data }: NodeProps = $props();
const { updateNodeData } = useSvelteFlow();
</script>
<div class="node">
<input class="nodrag" value={data.text ?? ''}
=> updateNodeData(id, { text: e.currentTarget.value })} />
<Handle type="target" position={Position.Left} />
<Handle type="source" position={Position.Right} />
</div>
<!-- App: register once at module scope, keys === node.type -->
<script lang="ts">
import { SvelteFlow, type NodeTypes } from '@xyflow/svelte';
import TextUpdaterNode from './TextUpdaterNode.svelte';
const nodeTypes: NodeTypes = { textUpdater: TextUpdaterNode };
// node: { id, type: 'textUpdater', position, data: { text: '' } }
</script>
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView />
Custom nodes have no default styles — style them yourself. Add nodrag to interactive elements
(inputs/buttons), nowheel to scroll containers, nopan to fixed overlays. After adding/removing
handles at runtime, call useUpdateNodeInternals(). Full patterns:
references/nodes-handles-subflows.md and
references/edges-connections.md.
Workflow when invoked
- Confirm the stack — Svelte 5 runes + SvelteKit 2 (
package.json, svelte.config.js). Ensure
@xyflow/svelte is installed (bun add @xyflow/svelte) and the stylesheet is imported once.
- Start from a template in
assets/ rather than from scratch — they already encode the five rules.
- Read the topic reference you need (table below) before using any prop/hook/util — don't guess
API names from React Flow or memory.
- Check the five rules against the result:
$state.raw + immutable updates? bind:? styles
imported? wrapper sized? hooks inside context using .current, nodeTypes at module scope?
- Verify every
.svelte / .svelte.ts file with the Svelte MCP svelte-autofixer before handing
back — mandatory for any Svelte code this skill produces.
References
| File |
Read when |
references/svelte-integration.md |
Start here. $state.raw + immutable updates, bind: / function bindings, hooks-return-.current, provider/context rules, SSR (width/height/handles), Svelte transition: with |global, TypeScript generics (Node/Edge unions, NodeProps/EdgeProps, BuiltInNode/BuiltInEdge, type guards). |
references/nodes-handles-subflows.md |
Custom nodes, NodeProps, nodeTypes, handles (multiple/ids/custom/Loose/dynamic + useUpdateNodeInternals), nodrag/nopan/nowheel, updateNodeData, NodeResizer/NodeToolbar, sub-flows (parentId, extent:'parent'). |
references/edges-connections.md |
Custom edges, EdgeProps, edgeTypes, path utils + custom SVG paths, BaseEdge/EdgeLabel, markers, reconnectable edges (EdgeReconnectAnchor), connection events & validation (isValidConnection, onbeforeconnect, ConnectionMode), addEdge, graph utils, delete-middle-node. |
references/hooks-components-api.md |
Every hook + useSvelteFlow methods, the built-in components (Background/Controls/MiniMap/Panel/ViewportPortal), and the grouped <SvelteFlow> prop & event catalog. |
references/layout-and-theming.md |
Auto-layout with dagre / d3-hierarchy / d3-force / elkjs (measure → compute → reassign), edge routing, theming (CSS variables, class overrides, dark mode, Tailwind layer(base)). |
references/troubleshooting-and-migration.md |
Every common warning/error + fix, "edges not showing" checklist, and the v0 → v1 migration map (writable→$state.raw, bind:, $props(), hooks .current, EdgeLabelRenderer→EdgeLabel, onEdgeCreate→onbeforeconnect). |
Templates (assets/)
| File |
What it is |
assets/Flow.svelte |
Golden-path flow: styles imported, $state.raw + bind:, Background/Controls/MiniMap, nodeTypes/edgeTypes wired, sized wrapper, onconnect via addEdge. |
assets/TextUpdaterNode.svelte |
Custom node: Handles + updateNodeData + nodrag, with scoped styles. |
assets/ButtonEdge.svelte |
Custom edge: getBezierPath + BaseEdge + EdgeLabel with a delete button (deleteElements). |
assets/flow.svelte.ts |
$state.raw graph store in a module + getters/setters for function bindings + immutable addNode/updateNodeData helpers. |
assets/dagre-layout.ts |
getLayoutedElements(nodes, edges, direction) — dagre helper returning new immutable arrays. |
1---2name: svelte-flow3description: Svelte Flow (@xyflow/svelte) — node-based UI / flow-graph / diagram editors for the rask SvelteKit frontend, biased to SvelteKit 2 + Svelte 5 (runes). Covers the <SvelteFlow /> component, nodes & edges, handles, custom nodes and custom edges, the built-in Background / Controls / MiniMap / Panel / ViewportPortal components, hooks (useSvelteFlow, useNodes, useEdges, useNodeConnections, useNodesData, useUpdateNodeInternals, useStore, …), edge-path utils (getBezierPath / getSmoothStepPath / getStraightPath), graph utils (addEdge, getConnectedEdges, getIncomers/getOutgoers), connection validation, reconnectable edges, sub-flows, theming, SSR, TypeScript generics, and external layouting (dagre / d3-hierarchy / d3-force / elkjs). Encodes the Svelte 5 reality that trips Claude up: nodes/edges are $state.raw and immutable, you bind:nodes / bind:edges, hooks return .current and must run inside the flow context, and the stylesheet must be imported. Ships copy-paste SvelteKit templates (a golden-path Flow.svelte, a custo4---56# Svelte Flow (@xyflow/svelte) for SvelteKit78Interactive node-based UIs — flow editors, graph/diagram canvases, workflow & pipeline builders — for9the rask **SvelteKit 2 + Svelte 5** frontend. This skill targets **Svelte Flow 1.0** (the Svelte 510rewrite). The library API is small; the hard part is Svelte 5 reactivity — and that's exactly where11generated code goes wrong.1213> Package: `@xyflow/svelte` (install with `bun add @xyflow/svelte`). This is **not** React Flow14> (`@xyflow/react`) — the APIs look similar but the components, props (lowercase events), and15> reactivity model differ. Never copy React Flow code into a Svelte file.1617## When to use1819- Building or editing a **node-and-edge canvas**: flow/workflow editors, pipeline/DAG builders,20 mind-maps, org charts, state-machine or RAG/agent graph visualizers, whiteboards.21- Anything needing **drag-to-connect handles**, custom node/edge components, a minimap, pan/zoom22 viewport, sub-flows (grouping), or programmatic graph manipulation.2324**Not for:** static, non-interactive diagrams that belong inline (use Mermaid, or the25[`architecture-diagram`](../architecture-diagram/SKILL.md) skill) · data charts (use LayerChart) ·26React (`@xyflow/react`).2728## The five rules (read before writing any Svelte Flow)2930These prevent ~every common bug. Details + the error each one causes are in31[`references/troubleshooting-and-migration.md`](references/troubleshooting-and-migration.md).32331. **State is `$state.raw` and immutable.** Declare `let nodes = $state.raw([...])` — never `$state(...)`34 (deep reactivity on every node/handle tanks performance). To change a node, **create a new object35 and reassign the array** (`nodes = nodes.map(...)`) or use `updateNode` / `updateNodeData`. Mutating36 `nodes[0].position.x = …` in place does **nothing**.372. **Bind, don't pass.** `<SvelteFlow bind:nodes bind:edges />`. Plain `{nodes}` won't write38 drag/select/connect changes back to your arrays. State in another module → use39 [function bindings](references/svelte-integration.md).403. **Import the stylesheet, once.** `import '@xyflow/svelte/dist/style.css';` in your entry/layout.41 Missing it = invisible edges, broken handles, and a warning. (Tailwind: import it `layer(base)`.)424. **The wrapper needs width *and* height.** `<SvelteFlow />` fills its parent; a zero-height `<div>`43 renders an empty canvas. Wrap it in an element with explicit or inherited dimensions.445. **Hooks run inside the flow and return `.current`.** `useNodes()`, `useSvelteFlow()`, etc. only work45 in a component rendered inside `<SvelteFlow>` or `<SvelteFlowProvider>`. Read reactive values via46 `.current`. Define `nodeTypes` / `edgeTypes` **once at module scope** (not inline) and make each key47 match the node/edge `type` string exactly.4849## Quick start5051```svelte52<script lang="ts">53 import { SvelteFlow, Background, Controls, MiniMap, BackgroundVariant } from '@xyflow/svelte';54 import '@xyflow/svelte/dist/style.css';55 import type { Node, Edge } from '@xyflow/svelte';5657 let nodes = $state.raw<Node[]>([58 { id: 'n1', type: 'input', position: { x: 0, y: 0 }, data: { label: 'Start' } },59 { id: 'n2', position: { x: 0, y: 120 }, data: { label: 'Next' } },60 ]);61 let edges = $state.raw<Edge[]>([62 { id: 'n1-n2', source: 'n1', target: 'n2' },63 ]);64</script>6566<div style:width="100%" style:height="100vh">67 <SvelteFlow bind:nodes bind:edges fitView colorMode="system">68 <Background variant={BackgroundVariant.Dots} gap={12} size={1} />69 <Controls />70 <MiniMap />71 </SvelteFlow>72</div>73```7475A node needs `id` + `position` + `data`; an edge needs `id` + `source` + `target`. `fitView` frames the76graph on load. Scaffold the fuller version from [`assets/Flow.svelte`](assets/Flow.svelte).7778## Golden path — pick the approach7980| You need… | Use | Where |81|---|---|---|82| A starting flow component | golden-path template | [`assets/Flow.svelte`](assets/Flow.svelte) |83| Custom node content (inputs, charts, multiple handles) | a Svelte component + `nodeTypes` | [`assets/TextUpdaterNode.svelte`](assets/TextUpdaterNode.svelte), [nodes ref](references/nodes-handles-subflows.md) |84| A custom edge (label, delete button, custom path) | `BaseEdge` + a path util + `EdgeLabel` | [`assets/ButtonEdge.svelte`](assets/ButtonEdge.svelte), [edges ref](references/edges-connections.md) |85| Read/mutate the graph imperatively | `useSvelteFlow()` + hooks (`.current`) | [hooks ref](references/hooks-components-api.md) |86| Graph/edge state in a shared module | `flow.svelte.ts` + function bindings | [`assets/flow.svelte.ts`](assets/flow.svelte.ts) |87| Auto-arrange nodes | external lib (dagre/elk) → reassign `nodes` | [`assets/dagre-layout.ts`](assets/dagre-layout.ts), [layout ref](references/layout-and-theming.md) |88| Persist/measure handles on the server | explicit `width`/`height`/`handles` | [SSR in svelte-integration ref](references/svelte-integration.md) |8990## Custom nodes & edges (the 80%)9192Custom nodes/edges are plain Svelte components that receive typed props. Register them once and refer to93them by the `type` key.9495```svelte96<!-- TextUpdaterNode.svelte — a custom node -->97<script lang="ts">98 import { Handle, Position, useSvelteFlow, type NodeProps } from '@xyflow/svelte';99 let { id, data }: NodeProps = $props();100 const { updateNodeData } = useSvelteFlow();101</script>102103<div class="node">104 <input class="nodrag" value={data.text ?? ''}105 oninput={(e) => updateNodeData(id, { text: e.currentTarget.value })} />106 <Handle type="target" position={Position.Left} />107 <Handle type="source" position={Position.Right} />108</div>109```110111```svelte112<!-- App: register once at module scope, keys === node.type -->113<script lang="ts">114 import { SvelteFlow, type NodeTypes } from '@xyflow/svelte';115 import TextUpdaterNode from './TextUpdaterNode.svelte';116 const nodeTypes: NodeTypes = { textUpdater: TextUpdaterNode };117 // node: { id, type: 'textUpdater', position, data: { text: '' } }118</script>119<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView />120```121122Custom nodes have **no default styles** — style them yourself. Add `nodrag` to interactive elements123(inputs/buttons), `nowheel` to scroll containers, `nopan` to fixed overlays. After adding/removing124handles at runtime, call `useUpdateNodeInternals()`. Full patterns:125[`references/nodes-handles-subflows.md`](references/nodes-handles-subflows.md) and126[`references/edges-connections.md`](references/edges-connections.md).127128## Workflow when invoked1291301. **Confirm the stack** — Svelte 5 runes + SvelteKit 2 (`package.json`, `svelte.config.js`). Ensure131 `@xyflow/svelte` is installed (`bun add @xyflow/svelte`) and the stylesheet is imported once.1322. **Start from a template** in `assets/` rather than from scratch — they already encode the five rules.1333. **Read the topic reference** you need (table below) before using any prop/hook/util — don't guess134 API names from React Flow or memory.1354. **Check the five rules** against the result: `$state.raw` + immutable updates? `bind:`? styles136 imported? wrapper sized? hooks inside context using `.current`, `nodeTypes` at module scope?1375. **Verify every `.svelte` / `.svelte.ts` file** with the Svelte MCP `svelte-autofixer` before handing138 back — mandatory for any Svelte code this skill produces.139140## References141142| File | Read when |143|---|---|144| [`references/svelte-integration.md`](references/svelte-integration.md) | **Start here.** `$state.raw` + immutable updates, `bind:` / function bindings, hooks-return-`.current`, provider/context rules, SSR (width/height/handles), Svelte `transition:` with `\|global`, TypeScript generics (Node/Edge unions, `NodeProps`/`EdgeProps`, `BuiltInNode`/`BuiltInEdge`, type guards). |145| [`references/nodes-handles-subflows.md`](references/nodes-handles-subflows.md) | Custom nodes, `NodeProps`, `nodeTypes`, handles (multiple/ids/custom/`Loose`/dynamic + `useUpdateNodeInternals`), `nodrag`/`nopan`/`nowheel`, `updateNodeData`, `NodeResizer`/`NodeToolbar`, sub-flows (`parentId`, `extent:'parent'`). |146| [`references/edges-connections.md`](references/edges-connections.md) | Custom edges, `EdgeProps`, `edgeTypes`, path utils + custom SVG paths, `BaseEdge`/`EdgeLabel`, markers, reconnectable edges (`EdgeReconnectAnchor`), connection events & validation (`isValidConnection`, `onbeforeconnect`, `ConnectionMode`), `addEdge`, graph utils, delete-middle-node. |147| [`references/hooks-components-api.md`](references/hooks-components-api.md) | Every hook + `useSvelteFlow` methods, the built-in components (`Background`/`Controls`/`MiniMap`/`Panel`/`ViewportPortal`), and the grouped `<SvelteFlow>` prop & event catalog. |148| [`references/layout-and-theming.md`](references/layout-and-theming.md) | Auto-layout with dagre / d3-hierarchy / d3-force / elkjs (measure → compute → reassign), edge routing, theming (CSS variables, class overrides, dark mode, Tailwind `layer(base)`). |149| [`references/troubleshooting-and-migration.md`](references/troubleshooting-and-migration.md) | Every common warning/error + fix, "edges not showing" checklist, and the v0 → v1 migration map (`writable`→`$state.raw`, `bind:`, `$props()`, hooks `.current`, `EdgeLabelRenderer`→`EdgeLabel`, `onEdgeCreate`→`onbeforeconnect`). |150151## Templates (`assets/`)152153| File | What it is |154|---|---|155| [`assets/Flow.svelte`](assets/Flow.svelte) | Golden-path flow: styles imported, `$state.raw` + `bind:`, Background/Controls/MiniMap, `nodeTypes`/`edgeTypes` wired, sized wrapper, `onconnect` via `addEdge`. |156| [`assets/TextUpdaterNode.svelte`](assets/TextUpdaterNode.svelte) | Custom node: `Handle`s + `updateNodeData` + `nodrag`, with scoped styles. |157| [`assets/ButtonEdge.svelte`](assets/ButtonEdge.svelte) | Custom edge: `getBezierPath` + `BaseEdge` + `EdgeLabel` with a delete button (`deleteElements`). |158| [`assets/flow.svelte.ts`](assets/flow.svelte.ts) | `$state.raw` graph store in a module + getters/setters for function bindings + immutable `addNode`/`updateNodeData` helpers. |159| [`assets/dagre-layout.ts`](assets/dagre-layout.ts) | `getLayoutedElements(nodes, edges, direction)` — dagre helper returning new immutable arrays. |