Node Wire Canvas
A node-based workflow canvas: freely draggable nodes, bezier wires between ports, drag-to-connect from any port, flowing pulses along edges while running, plus pan and zoom.
This is a WebberUI Pro component
The install command below needs a licence to run; without one it returns 401.
Turn a multi-step pipeline into a graph you can edit in place. Each node is a card with input ports on the left and output ports on the right, and ports are joined by cubic bezier wires. Drag a node anywhere and it springs onto the grid when released; drag a new wire out of an output port and it snaps to a valid input port as you get close; while the flow runs, pulses travel along every wire. The canvas pans with the wheel or by dragging empty space and zooms with Ctrl / ⌘ + wheel, and both nodes and edges are controllable data.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add "https://webberui.com/r/node-wire-canvas.en.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<NodeWireCanvas/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add "https://webberui.com/r/node-wire-canvas.en.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/node-wire-canvas.
Usage
import * as React from "react";
import {
NodeWireCanvas,
type NodeWireEdge,
type NodeWireNode,
} from "@/components/ui/node-wire-canvas";
const initialNodes: NodeWireNode[] = [
{ id: "prompt", x: 0, y: 0, title: "文字提示", outputs: [{ id: "prompt" }] },
{
id: "gen",
x: 280,
y: 40,
title: "影像生成",
inputs: [{ id: "prompt" }],
outputs: [{ id: "image" }],
},
];
const initialEdges: NodeWireEdge[] = [
{ id: "e1", from: { node: "prompt", port: "prompt" }, to: { node: "gen", port: "prompt" } },
];
export function Workflow() {
const [nodes, setNodes] = React.useState(initialNodes);
const [edges, setEdges] = React.useState(initialEdges);
const [running, setRunning] = React.useState(false);
return (
<NodeWireCanvas
nodes={nodes}
onNodesChange={setNodes}
edges={edges}
onEdgesChange={setEdges}
onConnect={(edge) => console.log("新連線", edge.id)}
showFlow={running}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | NodeWireNode[] | — | Node list (controlled) |
defaultNodes | NodeWireNode[] | [] | Initial node list (uncontrolled) |
onNodesChange | (nodes: NodeWireNode[]) => void | — | Called with the full new list when nodes move or are deleted |
edges | NodeWireEdge[] | — | Edge list (controlled) |
defaultEdges | NodeWireEdge[] | [] | Initial edge list (uncontrolled) |
onEdgesChange | (edges: NodeWireEdge[]) => void | — | Called with the full new list when edges are added or deleted |
onConnect | (edge: NodeWireEdge) => void | — | Called when the user creates a new edge |
showFlow | boolean | false | Show flowing pulses on every edge; when off, only edges whose source node is running flow |
snap | boolean | true | Snap nodes to the grid on release |
gridSize | number | 20 | Grid spacing (px); also the arrow-key nudge step |
stiffness | number | 400 | Spring stiffness of the snap-to-grid settle; higher settles faster |
flowDuration | number | — | Seconds for a pulse to travel one edge; falls back to --wb-nwc-flow-duration (1.6s) |
minZoom | number | 0.4 | Minimum zoom |
maxZoom | number | 2 | Maximum zoom |
defaultViewport | NodeWireViewport | — | Initial viewport; when omitted, all nodes are fitted into view on mount |
readOnly | boolean | false | Read-only: no node dragging, connecting or deleting; pan and zoom still work |
height | number | 480 | Canvas height (px) |
ariaLabel | string | "工作流程畫布" (Workflow canvas) | Accessible name of the canvas |
className | string | — | Extra classes merged onto the root element |
NodeWireNode
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique node id |
x | number | — | Card top-left x (canvas coordinates) |
y | number | — | Card top-left y (canvas coordinates) |
title | string | — | Title bar text |
inputs | NodeWirePort[] | [] | Input ports on the left |
outputs | NodeWirePort[] | [] | Output ports on the right |
status | "idle" | "running" | "done" | "error" | "idle" | Node status: grey dot, pulsing dot with a marching dashed outline, green check, red cross |
content | ReactNode | — | Card body, rendered below the port rows |
NodeWirePort
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Port id, unique within its node |
label | string | — | Display label; falls back to id |
NodeWireEdge
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique edge id; edges created by the user get "sourceNode.port->targetNode.port" |
from | { node: string; port: string } | — | Source (an output port) |
to | { node: string; port: string } | — | Target (an input port) |
NodeWireViewport
| Field | Type | Default | Description |
|---|---|---|---|
x | number | — | Horizontal pan (px) |
y | number | — | Vertical pan (px) |
zoom | number | — | Zoom factor |
CSS variables
| Variable | Default | Description |
|---|---|---|
--wb-nwc-accent | #6366f1 | Accent: selection ring, wire preview, running status dot |
--wb-nwc-flow-color | same as --wb-nwc-accent | Pulse and glow colour |
--wb-nwc-flow-duration | 1.6s | Time for a pulse to travel one edge |
--wb-nwc-march-duration | 0.8s | One loop of the running outline dashes |
--wb-nwc-dot | rgb(163 163 163 / 0.5) | Background dot grid colour |
How it works
- Coordinate system: the content layer expresses the whole canvas as
translate(panX, panY) scale(zoom), and the background dot grid spacing isgridSize × zoomso it tracks pan and zoom. The plain wheel pans, Ctrl / ⌘ + wheel (including trackpad pinch) zooms around the cursor, and dragging empty space pans too, matching common design tools - Port positions come from data: port coordinates are computed straight from the node
x/yand port index rather than measured from the DOM, so connected wires can be recomputed every frame during a drag without layout thrash. The drag itself is computed from pointer events (delta divided by zoom) and written back to data, not just applied as a transform - Snap to grid: with
snapon, releasing a node writes the snapped position to data first, thenuseSpring(tunable viastiffness) tweens the displayed position onto the grid, with wires following along - Wires: each edge is a cubic bezier
M p1 C p1+dx p2−dx p2withdx = max(60, |Δx| / 2). Dragging out of an output port shows a dashed preview that snaps and highlights within 16px of a valid input port; self-connections and duplicates are rejected. Select a wire and press Delete to remove it - Flow: when
showFlowis true, or an edge's source node isrunning, three pulses travel along that edge via CSSoffset-pathon the same path, staggered by a third of a cycle; they pause while the canvas is scrolled out of view - How it differs from patchbay-integrations: the patchbay is a fixed two-column integration board; here nodes can be laid out freely and edges are data you can add, delete and control — a fit for workflow editors
Accessibility
- The canvas is
role="application"with anaria-label(default "工作流程畫布", Workflow canvas) andsr-onlyinstructions; creating, deleting or cancelling a connection is announced througharia-live - Nodes are Tab-focusable (
role="group", named with title and status); arrow keys move one grid step, Shift plus arrow keys moves five, and Delete removes the node with its wires; all disabled whenreadOnly - Ports are
buttons: press Enter on an output port to enter connect mode, Tab to a target input port and press Enter to finish, Esc cancels; with a mouse you can also click an output port, then an input port. Each wire is Tab-focusable too and Delete removes it - The + and - keys zoom and 0 resets the view; the corner toolbar has Zoom out, Zoom in and Fit all buttons
- When the user's system has reduce motion turned on, flow pulses are not shown (flowing edges get a solid highlight instead), snapping jumps straight into place without a tween, and the running status dot and dashed outline stay still
Flashcard Deck
A study flashcard deck React component. Tap or press Space to flip in 3D, drag or use arrow keys to sort into known and unknown, with a progress bar and an end screen showing an accuracy ring, restart and review-unknown options.
Radius & Shadow Tuner
Drag handles to adjust a box's corner radius, shadow and padding with live readouts and the resulting CSS — ideal for demonstrating design tokens in docs.