React Flow (XYFlow) builds interactive node-based UIs — import { ReactFlow, useNodesState, useEdgesState, addEdge } from "@xyflow/react" is the entry point. const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes) and const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) manage state. <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} nodeTypes={nodeTypes}> renders the canvas. onConnect: (params) => setEdges((eds) => addEdge(params, eds)) wires connections. Custom nodes: register in nodeTypes object and create components with Handle from @xyflow/react for source/target ports. useReactFlow() returns { fitView, setCenter, zoomIn, zoomOut, getNodes, setNodes } for programmatic control. Edge types: type: "straight" | "step" | "smoothstep" | "bezier" — animated: true adds dash animation. markerEnd: { type: MarkerType.ArrowClosed } adds arrowheads. <MiniMap>, <Controls>, <Background variant="dots"> are panel components. <NodeToolbar> renders a floating toolbar on selected nodes. Auto-layout with dagre: dagre.layout(graph) then map graph.node(id).x/y back to node positions. Claude Code generates React Flow workflow editors, pipeline builders, and mind-map UIs.
CLAUDE.md for React Flow
## React Flow Stack
- Version: @xyflow/react >= 12
- Init: import { ReactFlow, useNodesState, useEdgesState, addEdge, Handle, Position, useReactFlow, MiniMap, Controls, Background } from "@xyflow/react"
- Wrap app in: <ReactFlowProvider> (needed for useReactFlow outside ReactFlow component)
- State: const [nodes, setNodes, onNodesChange] = useNodesState([]) — same for edges
- Connect: const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges])
- Custom node: ({ data, selected }) => <div>...</div> — register in nodeTypes={{ myType: MyNodeComponent }}
- Handle: <Handle type="source" position={Position.Right} id="out" /> — type is "source" | "target"
- Node schema: { id: string, type?: string, position: { x, y }, data: Record<string, unknown> }
- Edge schema: { id: string, source: string, target: string, sourceHandle?: string, targetHandle?: string }
React Flow Canvas with Custom Nodes
// components/flow/WorkflowEditor.tsx — full workflow editor
"use client"
import { useCallback, useRef } from "react"
import {
ReactFlow,
ReactFlowProvider,
useNodesState,
useEdgesState,
addEdge,
useReactFlow,
MiniMap,
Controls,
Background,
BackgroundVariant,
Handle,
Position,
NodeToolbar,
MarkerType,
type Node,
type Edge,
type Connection,
type NodeTypes,
} from "@xyflow/react"
import "@xyflow/react/dist/style.css"
// ── Custom node types ──────────────────────────────────────────────────────
type InputNodeData = { label: string; value?: string }
type ProcessNodeData = { label: string; operation: string }
type OutputNodeData = { label: string; format: string }
function InputNode({ data, selected }: { data: InputNodeData; selected?: boolean }) {
return (
<div className={`rounded-lg border-2 bg-white px-4 py-3 shadow-md min-w-[140px] ${selected ? "border-indigo-500" : "border-gray-200"}`}>
<NodeToolbar isVisible={selected} position={Position.Top}>
<button className="rounded bg-red-100 px-2 py-0.5 text-xs text-red-600 hover:bg-red-200">Delete</button>
</NodeToolbar>
<div className="flex items-center gap-2">
<span className="text-green-500">◈</span>
<div>
<div className="text-xs font-semibold uppercase tracking-wide text-gray-400">Input</div>
<div className="font-medium text-gray-800">{data.label}</div>
</div>
</div>
<Handle type="source" position={Position.Right} className="!bg-green-500 !w-3 !h-3" />
</div>
)
}
function ProcessNode({ data, selected }: { data: ProcessNodeData; selected?: boolean }) {
return (
<div className={`rounded-lg border-2 bg-white px-4 py-3 shadow-md min-w-[160px] ${selected ? "border-indigo-500" : "border-purple-200"}`}>
<Handle type="target" position={Position.Left} id="in" className="!bg-purple-400 !w-3 !h-3" />
<div className="flex items-center gap-2">
<span className="text-purple-500">⚙</span>
<div>
<div className="text-xs font-semibold uppercase tracking-wide text-gray-400">Process</div>
<div className="font-medium text-gray-800">{data.label}</div>
<div className="text-xs text-purple-600 mt-0.5">{data.operation}</div>
</div>
</div>
<Handle type="source" position={Position.Right} id="out" className="!bg-purple-400 !w-3 !h-3" />
</div>
)
}
function OutputNode({ data, selected }: { data: OutputNodeData; selected?: boolean }) {
return (
<div className={`rounded-lg border-2 bg-white px-4 py-3 shadow-md min-w-[140px] ${selected ? "border-indigo-500" : "border-blue-200"}`}>
<Handle type="target" position={Position.Left} className="!bg-blue-400 !w-3 !h-3" />
<div className="flex items-center gap-2">
<span className="text-blue-500">◉</span>
<div>
<div className="text-xs font-semibold uppercase tracking-wide text-gray-400">Output</div>
<div className="font-medium text-gray-800">{data.label}</div>
<div className="text-xs text-blue-600 mt-0.5">{data.format}</div>
</div>
</div>
</div>
)
}
const nodeTypes: NodeTypes = {
input: InputNode as any,
process: ProcessNode as any,
output: OutputNode as any,
}
// ── Initial graph ─────────────────────────────────────────────────────────
const initialNodes: Node[] = [
{ id: "1", type: "input", position: { x: 50, y: 150 }, data: { label: "CSV File" } },
{ id: "2", type: "process", position: { x: 260, y: 80 }, data: { label: "Parse", operation: "csv → json" } },
{ id: "3", type: "process", position: { x: 260, y: 220 }, data: { label: "Validate", operation: "schema check" } },
{ id: "4", type: "process", position: { x: 480, y: 150 }, data: { label: "Transform", operation: "normalize fields" } },
{ id: "5", type: "output", position: { x: 700, y: 150 }, data: { label: "Database", format: "PostgreSQL" } },
]
const initialEdges: Edge[] = [
{ id: "e1-2", source: "1", target: "2", animated: true, markerEnd: { type: MarkerType.ArrowClosed } },
{ id: "e1-3", source: "1", target: "3", animated: true, markerEnd: { type: MarkerType.ArrowClosed } },
{ id: "e2-4", source: "2", target: "4", markerEnd: { type: MarkerType.ArrowClosed } },
{ id: "e3-4", source: "3", target: "4", markerEnd: { type: MarkerType.ArrowClosed } },
{ id: "e4-5", source: "4", target: "5", type: "smoothstep", markerEnd: { type: MarkerType.ArrowClosed } },
]
// ── Toolbar ───────────────────────────────────────────────────────────────
function Toolbar({ onAdd }: { onAdd: (type: "input" | "process" | "output") => void }) {
return (
<div className="absolute top-4 left-4 z-10 flex gap-2 bg-white/90 backdrop-blur rounded-xl border shadow-sm p-2">
{(["input", "process", "output"] as const).map((type) => (
<button
key={type}
onClick={() => onAdd(type)}
className="px-3 py-1.5 rounded-lg text-sm font-medium bg-gray-100 hover:bg-indigo-50 hover:text-indigo-700 transition-colors capitalize"
>
+ {type}
</button>
))}
</div>
)
}
// ── Main editor ───────────────────────────────────────────────────────────
function WorkflowEditorInner() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
const { fitView, screenToFlowPosition } = useReactFlow()
const idCounter = useRef(initialNodes.length + 1)
const onConnect = useCallback(
(params: Connection) =>
setEdges((eds) =>
addEdge({ ...params, markerEnd: { type: MarkerType.ArrowClosed } }, eds),
),
[setEdges],
)
const addNode = useCallback(
(type: "input" | "process" | "output") => {
const id = String(++idCounter.current)
const defaults = {
input: { label: "New Input", value: "" },
process: { label: "New Process", operation: "transform" },
output: { label: "New Output", format: "json" },
}
setNodes((nds) => [
...nds,
{
id,
type,
position: screenToFlowPosition({ x: 200 + Math.random() * 200, y: 150 + Math.random() * 100 }),
data: defaults[type],
},
])
},
[setNodes, screenToFlowPosition],
)
return (
<div className="relative w-full h-[600px] rounded-xl border overflow-hidden bg-gray-50">
<Toolbar onAdd={addNode} />
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
defaultEdgeOptions={{ type: "smoothstep", markerEnd: { type: MarkerType.ArrowClosed } }}
fitView
proOptions={{ hideAttribution: true }}
>
<MiniMap nodeColor={(n) => {
if (n.type === "input") return "#22c55e"
if (n.type === "process") return "#a855f7"
return "#3b82f6"
}} />
<Controls showFitView showZoom showInteractive />
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#d1d5db" />
</ReactFlow>
</div>
)
}
export default function WorkflowEditor() {
return (
<ReactFlowProvider>
<WorkflowEditorInner />
</ReactFlowProvider>
)
}
Auto-Layout with Dagre
// lib/flow/layout.ts — automatic left-to-right dagre layout
import dagre from "@dagrejs/dagre"
import type { Node, Edge } from "@xyflow/react"
const NODE_WIDTH = 180
const NODE_HEIGHT = 80
/** Apply dagre LR layout to nodes and reset their positions */
export function applyDagreLayout(
nodes: Node[],
edges: Edge[],
direction: "LR" | "TB" = "LR",
): Node[] {
const g = new dagre.graphlib.Graph()
g.setDefaultEdgeLabel(() => ({}))
g.setGraph({ rankdir: direction, nodesep: 60, ranksep: 120 })
nodes.forEach((n) => g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT }))
edges.forEach((e) => g.setEdge(e.source, e.target))
dagre.layout(g)
return nodes.map((n) => {
const { x, y } = g.node(n.id)
return { ...n, position: { x: x - NODE_WIDTH / 2, y: y - NODE_HEIGHT / 2 } }
})
}
// Usage in component:
// const layouted = applyDagreLayout(nodes, edges, "LR")
// setNodes(layouted)
// window.requestAnimationFrame(() => fitView({ padding: 0.2 }))
Serialize and Restore Graph
// lib/flow/persistence.ts — save/load workflow as JSON
import type { Node, Edge } from "@xyflow/react"
export type FlowGraph = { nodes: Node[]; edges: Edge[]; version: number }
export function serializeFlow(nodes: Node[], edges: Edge[]): string {
const graph: FlowGraph = { nodes, edges, version: 1 }
return JSON.stringify(graph)
}
export function deserializeFlow(json: string): FlowGraph {
const parsed = JSON.parse(json) as FlowGraph
if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) {
throw new Error("Invalid flow JSON")
}
return parsed
}
// app/api/workflows/[id]/route.ts — save workflow to DB
// export async function PUT(req: Request, { params }: { params: { id: string } }) {
// const { nodes, edges } = await req.json()
// await db.workflow.update({ where: { id: params.id }, data: { graph: serializeFlow(nodes, edges) } })
// return NextResponse.json({ ok: true })
// }
For the Cytoscape.js alternative when needing graph theory algorithms (shortest path, community detection, PageRank), large graphs with thousands of nodes rendered in WebGL via canvas, or biology/network-science style diagrams — Cytoscape excels at graph analytics while React Flow is the go-to for interactive workflow editors and node-based UIs where drag-and-drop authoring is the primary interaction, see the Cytoscape guide. For the Mermaid alternative when generating diagrams from a text DSL (sequence, Gantt, ER) that renders in Markdown without any interactive editing — Mermaid is perfect for documentation while React Flow is for application-grade interactive canvases where users build and edit graphs at runtime, see the Mermaid guide. The Claude Skills 360 bundle includes React Flow skill sets covering workflow editors, custom nodes, and dagre auto-layout. Start with the free tier to try node graph generation.