Smart edges for React Flow that route around your nodes instead of straight through them.
Drop-in custom edges that use grid-based A* pathfinding to find a clean path between nodes, plus floating endpoints, draggable waypoints, and circuit-style hops over crossing wires.
React Flow's built-in edges draw a direct line from source to target, which often means edges cut straight across your nodes. React Flow Smart Edge computes a path that goes around them, so your graphs stay readable even as they grow and as nodes move.
It's a tiny, dependency-light library (just @xyflow/react as a peer) that ships ready-to-use edge components and a low-level API for building your own.
- Grid-based A* / jump-point pathfinding finds a path that never crosses your nodes.
- Five edge styles: smart equivalents of every React Flow edge (bezier, straight, step, smooth-step, and simple-bezier).
- Every smart edge routes through one shared
SmartEdgeProvider, off the main thread on a Web Worker by default, so large graphs stay responsive. (new!) - Edges whose direct line is already clear skip pathfinding entirely (
routeOnlyWhenBlocked, on by default). (new!) - Moving a node only re-routes the edges whose corridor it actually entered or left; everything else is served from an LRU route cache. (new!)
- Step edges can draw a small bridge arc where they cross each other, so intersections read cleanly.
- Floating edges connect to the nearest node border instead of a fixed handle.
- Editable waypoints let you drag control points to reshape a route; each segment still avoids nodes.
- Checkpoints route through fixed points without the editing UI.
- Avoid areas keep edges clear of arbitrary regions (e.g. labels), not just nodes.
- Subflow aware routing works correctly inside React Flow groups/subflows.
- If no path is found, or while a route is deferred, the edge drops back to the native React Flow edge.
- Swap the pathfinding or SVG drawing functions, or build custom edges with
useSmartEdgePathorgetSmartEdge. - Written in strict TypeScript, with browser-based interaction tests.
npm install @tisoap/react-flow-smart-edgeRequires React Flow v12+ (@xyflow/react).
Smart edges route through a SmartEdgeProvider, which owns the routing worker and needs your current nodes to know what to route around. That means nodes must be controlled (useNodesState, or your own state), not just handed to React Flow as defaultNodes:
import { ReactFlow, useNodesState } from "@xyflow/react";
import {
SmartEdgeProvider,
SmartBezierEdge,
} from "@tisoap/react-flow-smart-edge";
import "@xyflow/react/dist/style.css";
const initialNodes = [
{ id: "1", data: { label: "Node 1" }, position: { x: 300, y: 100 } },
{ id: "2", data: { label: "Node 2" }, position: { x: 300, y: 200 } },
];
const edges = [{ id: "e21", source: "2", target: "1", type: "smart" }];
const edgeTypes = { smart: SmartBezierEdge };
export function Graph() {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
return (
<SmartEdgeProvider nodes={nodes}>
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
defaultEdges={edges}
edgeTypes={edgeTypes}
fitView
/>
</SmartEdgeProvider>
);
}Without a SmartEdgeProvider ancestor, smart edges warn once in development and render their native (non-routed) fallback edge, so a graph without one still renders, just without routing.
| Export | React Flow equivalent |
|---|---|
SmartBezierEdge |
BezierEdge |
SmartStraightEdge |
StraightEdge |
SmartStepEdge |
StepEdge |
SmartSmoothStepEdge |
SmoothStepEdge |
SmartSimpleBezierEdge |
SimpleBezierEdge |
SmartFloatingEdge |
Floating edges example |
SmartEditableEdge |
Editable edge example |
SmartCheckpointEdge |
No equivalent |
Configure any preset with createSmartEdge, or see Custom edges below to build your own:
import { createSmartEdge } from "@tisoap/react-flow-smart-edge";
const edgeTypes = {
// finer routing grid:
fineStep: createSmartEdge("step", { gridRatio: 5 }),
};Give the step variants the hops option and crossing wires bridge over each
other like a schematic. The edge on top draws a small arc over the one beneath.
Hops still draw when routeOnlyWhenBlocked leaves an edge on its native step
path, not only after A* has detoured around a node:
import { createSmartEdge } from "@tisoap/react-flow-smart-edge";
const edgeTypes = {
hop: createSmartEdge("step", { hops: true }),
// or smooth-step with rounded corners + bridges:
smoothHop: createSmartEdge("smoothstep", { hops: { borderRadius: 8 } }),
};See the hops docs for tuning and a live demo.
Build a fully custom edge with useSmartEdgePath. It registers your edge's geometry with the nearest SmartEdgeProvider and returns its routed path, or null/"clear" while there is nothing to draw yet:
import { BaseEdge, BezierEdge } from "@xyflow/react";
import { useSmartEdgePath } from "@tisoap/react-flow-smart-edge";
import type { EdgeProps } from "@xyflow/react";
function MySmartEdge(props: EdgeProps) {
const { route } = useSmartEdgePath({ ...props, preset: "bezier" });
if (!route || route.kind === "clear") {
return <BezierEdge {...props} />;
}
return (
<BaseEdge
id={props.id}
path={route.svgPathString}
markerEnd={props.markerEnd}
/>
);
}
const edgeTypes = { custom: MySmartEdge };For synchronous, main-thread routing outside a provider (SSR, tests, your own batching), call getSmartEdge directly. See the custom edges guide.
Every smart edge registers with the nearest SmartEdgeProvider, which batches every registered edge's routing together and, by default, runs that batch on a background Web Worker so pathfinding never blocks the main thread:
- Web Worker routing runs by default. Nodes and edges paint immediately on mount, before the first routing batch has even run (smart edges fall back to their native path until routed), so a large graph never freezes the tab while it computes routes.
- Edges route only when blocked (
routeOnlyWhenBlocked: trueby default). An edge whose straight line between endpoints is already clear skips pathfinding and renders the preset's native path, so a typical graph only pays for A* on the edges that actually need it. - Each edge routes on a corridor-cropped, typed-array grid: a small
Uint8Arraygrid cropped around its own endpoints first, widening the crop only if that fails, instead of always rebuilding a grid over the whole graph. - Node moves invalidate incrementally, into an LRU route cache. Moving one node only re-routes the edges whose corridor it actually entered or left; every other edge's cached route (keyed by its own obstacle set) is reused untouched.
- A dragged edge keeps its native path until drop, then routes. Set
routeWhileDragging: trueto re-route during the drag. onMetricsreports runtime metrics for every completed batch:batchLatencyMsandmainThreadBlockingMs,routed/clear/cacheHits/deferred/unchangedcounts, and whether it ran on the worker or the main thread.- A benchmark suite (
npm run bench) measured the pipeline against the old (pre-v5) object-based grid before that legacy engine was deleted from the repo. Seebench/RESULTS.mdfor the historical numbers: grid construction was roughly 20-24x faster, A* pathfinding 5.4x (diagonal) to 9.8x (orthogonal) faster, and jump-point search 6.8x faster; the corridor-cropped grid cut routing on a 750-node graph to about 1.7x over always building the full grid. On the #69 750-node / 1,125-edge scenario that used to freeze the tab, every node still paints instantly, and the first routing batch completes off the main thread in about 2.2s.
<SmartEdgeProvider nodes={nodes} onMetrics={(metrics) => console.log(metrics)}>
<ReactFlow nodes={nodes} edges={edges} edgeTypes={edgeTypes} />
</SmartEdgeProvider>See the performance guide and the SmartEdgeProvider reference for every option.
v5 makes SmartEdgeProvider required: every smart edge needs one as an ancestor to route (it warns once and falls back to its native edge without one). Nodes are passed straight to the provider, so apps using uncontrolled defaultNodes need to lift that state, as shown in Quick start. routeOnlyWhenBlocked also now defaults to true, so an edge with a clear direct line renders its native path unless you opt back into always routing.
See the migration guide for the full before/after, the options that restore v4 behavior, and the mapping from the removed 4.13 batch-routing API to SmartEdgeProvider / useSmartEdgePath.
Full documentation: tisoap.github.io/react-flow-smart-edge/docs
Guides, the full API reference, and live interactive demos for every feature.
Interactive Storybook examples are also published on Chromatic.
npm ci
npm run storybook # demos + tests (port 6006)
npm run docs # Docusaurus dev server → http://localhost:3000/docs
npm run verify # lint, types, spellcheck, coverage tests, full build
npm run check # lint, types, spellcheck
npm run test # Storybook interaction tests
npm run build-docs # static docs site → website/build