Workflow Canvas React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React workflow builder: a node editor for automations with a trigger, actions, AI steps and yes / no branches you connect and watch run.
Steps are cards on a dotted canvas, joined by rounded step lines; a condition splits into Yes and No branches with a label on each line. Drag the canvas to pan, scroll or swipe with two fingers to pan, pinch (or Ctrl and the wheel) to zoom. Click a step to select it and open its settings in a panel on the right; drag it to move it (it snaps to a 16px grid and the move is one undo step). Drag from the dot under a step to another step to connect them: a connection into a trigger, one that would make a loop or a second line from the same branch is refused and the line turns red. Drop the line on empty space and the add menu opens there. Hover a line (or select it) for its + button, which inserts a step in between; a step with nothing after it shows a dashed + stub, and a branch that goes nowhere names itself next to it. The add menu searches the step catalog by name, kind or group. Right-click a step for Duplicate, Skip and Delete, or the empty canvas to add a step where you clicked; deleting a connected step asks first and a step in the middle hands its parent to its child. The toolbar adds (after the selected step, on the selected line, or in a free spot), undoes and redoes, tidies the layout (steps glide to their new places), zooms, fits and locks, and shows Run when onRun is set. Shift and click, or Shift and a drag on the canvas, selects several steps; Delete then asks once for all of them. In run mode editing locks, the line into the running step flows, markers turn from queued to running to done or failed, the view follows the running step unless you just panned, and a run that ends with every step done pops its markers once. On a phone one finger pans and two pinch; tap a step to select it, then drag it (an unselected step never moves by accident); long-press a step for its menu or the empty canvas to add a step; handles take 44px of finger reaching away from the card; the toolbar centers above the home indicator with 44px buttons, folding tidy, zoom and lock into a menu; settings open in a sheet that stops at half height, the selected step scrolls above it, and the canvas stays usable. With a keyboard: Tab reaches the canvas, the toolbar (arrow keys move inside it) and every step and connection; Enter or Space selects, Enter again moves into the settings and Escape goes back; arrows move the selected step (Shift for 4 times as far); Delete or Backspace removes the selection; A adds a step after the selection or on the selected line; Ctrl or Cmd with Z undoes, with Shift and Z (or Y) redoes, with D duplicates and with A selects everything; + and - zoom, Shift and 1 fits; the menu key or Shift and F10 opens a step's menu. Validation problems ring the step in red and are listed in its tooltip and settings; changes are announced to screen readers.
Workflow Canvas is a Pro React component in the React AI components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add workflow-canvas, together with Alert Dialog, Button, Drawer, Dropdown Menu, Input and 14 more and the npm packages @xyflow/react, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add workflow-canvasWorkflow Canvas is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import WorkflowCanvas from @/components/ui/workflow-canvas.
import { useState } from "react"
import { CalendarClock, Flag, Mail, Search, Split } from "lucide-react"
import {
WorkflowCanvas,
layoutWorkflow,
type WorkflowEdge,
type WorkflowNodeDef,
type WorkflowValue,
} from "@/components/ui/workflow-canvas"
// the component imports @xyflow/react/dist/base.css itself; Next and Vite accept a package css import in a client component
import { AlertDialogProvider } from "@/components/ui/alert-dialog"
// the step catalog, at module scope so it keeps its identity
const nodeTypes: WorkflowNodeDef[] = [
{ type: "schedule", kind: "trigger", label: "Daily at a set time", icon: <CalendarClock />, group: "Triggers" },
{ type: "find-invoices", kind: "action", label: "Find invoices", icon: <Search />, group: "CRM actions" },
{ type: "condition", kind: "condition", label: "Condition", icon: <Split />, group: "Logic" },
{ type: "send-email", kind: "action", label: "Send email", icon: <Mail />, group: "Communication" },
{ type: "end", kind: "end", label: "End", icon: <Flag />, group: "Logic" },
]
const edges: WorkflowEdge[] = [
{ id: "e1", source: "start", target: "find" },
{ id: "e2", source: "find", target: "check" },
{ id: "e3", source: "check", target: "send", sourceHandle: "yes" },
{ id: "e4", source: "check", target: "stop", sourceHandle: "no" },
]
const initial: WorkflowValue = {
nodes: layoutWorkflow(
[
{ id: "start", type: "schedule", position: { x: 0, y: 0 }, data: { title: "Daily at 9:00 AM" } },
{ id: "find", type: "find-invoices", position: { x: 0, y: 0 }, data: { title: "Find overdue invoices" } },
{ id: "check", type: "condition", position: { x: 0, y: 0 }, data: { title: "Balance over $1,000?" } },
{ id: "send", type: "send-email", position: { x: 0, y: 0 }, data: { title: "Send reminder", subtitle: "Template: Payment reminder" } },
{ id: "stop", type: "end", position: { x: 0, y: 0 }, data: { title: "Nothing to send" } },
],
edges,
{ nodeTypes },
),
edges,
}
export function AutomationEditor() {
const [workflow, setWorkflow] = useState(initial)
return (
<div className="h-[560px]">
<WorkflowCanvas
nodeTypes={nodeTypes}
value={workflow}
onValueChange={setWorkflow}
onRun={() => console.log("Run", workflow)}
/>
{/* once near the root: confirms deleting a connected step */}
<AlertDialogProvider />
</div>
)
}Props
nodeTypes*WorkflowNodeDef[]The step catalog: { type, kind (trigger | action | condition | ai | delay | end), label, description, icon, group, tone, outputs, defaults, keywords }. It fills the add menu (grouped by group), gives each card its icon and kind, and a condition's outputs (Yes / No unless you name your own). Keep it at module scope.
valueWorkflowValueThe graph: { nodes: { id, type, position, data: { title, subtitle, status, error, config, disabled } }[], edges: { id, source, target, sourceHandle, label }[] }. A drag commits once, on drop; a value that arrives mid-drag waits until the drop. Steps that all sit at 0, 0 are laid out once.
defaultValueWorkflowValueThe starting graph while uncontrolled; an empty canvas (the trigger picker) without it.
onValueChange(value: WorkflowValue) => voidEvery committed change: an add, a delete, a connection, a drop, a tidy, an inspector edit, an undo.
selectedIdstring | nullThe selected step, which opens the inspector. Several selected steps (Shift and click, a box) report null.
defaultSelectedIddefault""string | nullThe step selected at first while uncontrolled, so its settings open with the canvas. The playground offers the steps of the demo workflow; "" selects nothing.
onSelectedIdChange(id: string | null) => voidThe selection changed. With inspector="none" this is where the app opens its own panel.
directiondefault"vertical""vertical" | "horizontal"The flow: top to bottom (reads best on phones) or left to right. Handles, the dashed + stubs, the label of a branch and the tidy layout follow it.
readOnlydefaultfalsebooleanView only: pan, zoom, select and inspect. The editing tools, the + buttons and the step menu go away and the inspector shows its fields read only.
showMinimapdefault"auto""auto" | "always" | "never"auto: from 768px, while part of the graph is outside the view (after zooming in or panning). always also shows it on phones, in the top corner. The minimap pans and zooms the canvas.
showControlsdefaulttruebooleanThe floating toolbar: add, undo, redo, tidy up, zoom, fit, lock and Run.
backgrounddefault"dots""dots" | "lines" | "none"The pattern under the steps, drawn in a faint foreground tint so it works in both themes.
snapToGriddefaulttruebooleanDragged steps land on a 16px grid, and the arrow keys move a step by it (5px without the grid).
fitOnMountdefaulttruebooleanFits the whole graph into view on mount, clear of the toolbar and never above 100%, and again when a canvas that mounted hidden (a tab, a sheet) becomes visible. A flow too wide to read at that size (a long horizontal flow in a narrow column) opens at 50% from its trigger instead.
inspectordefault"auto""auto" | "none"auto: the selected step's settings open in a panel on the right (at the bottom of a narrow canvas) and in a half-height sheet on phones. none: no built-in panel; show your own from onSelectedIdChange.
renderInspector(node: WorkflowNode, api: { update: (data: Partial<WorkflowNode["data"]>) => void; def: WorkflowNodeDef; fields: ReactNode; readOnly: boolean }) => ReactNodeThe inspector body for a step: its settings form. update merges into the step's data (one undo step per 500ms burst); fields is the default name, description and skip fields; readOnly is true while read only, locked or running.
onRun() => voidShows a Run button in the toolbar. Start the run, then drive running and each step's data.status.
runningdefaultfalsebooleanRun mode: editing locks, steps show node.data.status as a marker, the line into a running step flows, finished lines turn green, and the view follows the running step unless the person panned in the last 3 s.
confirmDeletedefaulttruebooleanAsks before deleting a step that has connections (through useConfirm, so mount AlertDialogProvider once).
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The selection ring, the dragged connection, a flowing line, the add button and the + buttons.
colordefault""stringAny CSS color; replaces the tone. Leave empty to use the tone.
labelsPartial<WorkflowCanvasLabels>Every string, English by default (DEFAULT_WORKFLOW_CANVAS_LABELS): canvas, roleDescription, step, instructions, the toolbar's names (addStep, undo, redo, tidy, zoomIn, zoomOut, fit, lock, unlock, more, run, running), searchSteps, noSteps, emptyTitle, emptyDescription, insert, yes, no, the inspector's (inspector, title, subtitle, disabled, duplicate, delete, issuesTitle), deleteTitle(title), deleteManyTitle(count), deleteDescription, kinds, status, issues (the validation messages), announce, nodeLabel(step) and edgeLabel(from, to, label).
classNamesPartial<Record<"flow" | "toolbar" | "minimap" | "menu" | "inspector" | "header" | "body" | "footer" | "empty" | "node" | "edgeLabel" | "insert", string>>Extra classes for the parts. The root takes className; drop its frame with className="rounded-none bg-transparent".
flowPropsPartial<ReactFlowProps<WorkflowFlowNode, WorkflowFlowEdge>>Extra props for the underlying ReactFlow element (minZoom, maxZoom, zoomOnScroll, proOptions...). The xyflow attribution is hidden by default, which its MIT license allows; pass proOptions to show it.
classNamestringClasses for the root, merged last. It fills its parent (at least 420px tall) in a rounded-3xl bg-card frame; give the parent a height.
Parts
WorkflowNodeCard
componentThe look of one step on its own: the kind's icon tile, the kind, the title, the subtitle and the run marker, with the selected, invalid and skipped looks. For previews, template galleries and lists. Props: { node?: WorkflowNode; def?: WorkflowNodeDef; title?; subtitle?; kind?; icon?; status?; selected?; disabled?; invalid?; tone?; color?; labels?; classNames? }.
layoutWorkflow
functionA layered layout: ranks by the longest path from the trigger, a condition's branches side by side in output order, each row spread evenly around its parents. Returns the steps with new positions (top-left at 0, 0). Signature: (nodes: WorkflowNode[], edges: WorkflowEdge[], options?: { direction?; gapX? = 64; gapY? = 56; nodeWidth? = 260; nodeHeight? = 64; sizes?; nodeTypes? }) => WorkflowNode[].
validateWorkflow
functionThe problems that stop an automation from running: no trigger or more than one, a connection into a trigger, loops, a condition branch that goes nowhere, unknown step types and connections to missing steps. Signature: (value: WorkflowValue, nodeTypes: WorkflowNodeDef[], messages?: Partial<WorkflowIssueMessages>) => { nodeId?: string; edgeId?: string; message: string }[].
createWorkflowNode
functionA new step from its definition, with a fresh id and the def's defaults (call it in handlers, not while rendering). Signature: (def: WorkflowNodeDef, position?: { x: number; y: number }) => WorkflowNode.
DEFAULT_WORKFLOW_CANVAS_LABELS
constantThe English strings, to spread under your translations. Type: WorkflowCanvasLabels.
DEFAULT_WORKFLOW_ISSUE_MESSAGES
constantThe English validation messages that validateWorkflow and the canvas use: noTrigger, multipleTriggers, triggerInput, cycle, missingNode, unknownType(type), unconnectedOutput(output), plus the "Yes" / "No" branch names. Pass a partial copy to validateWorkflow or as labels.issues to translate them. Type: WorkflowIssueMessages.
WorkflowValue
typeThe automation: { nodes, edges }. A node is { id, type, position, data: { title, subtitle?, status?, error?, config?, disabled? } }; an edge is { id, source, target, sourceHandle?, label? }, where sourceHandle names a condition's output.
WorkflowNodeDef
typeOne step type for the add menu: { type, kind, label, description?, icon, group?, tone?, outputs?, defaults?, keywords? }. kind is trigger, action, condition, ai, delay or end.
Examples
Horizontal, brand tone
direction="horizontal" for a left to right flow; tone="brand" colors the selection, the add button and the + buttons.
Empty
No steps yet: the canvas offers the triggers from nodeTypes, and only while there is none.
Read-only run history
readOnly with node.data.status from a finished run; pan, zoom and inspect, nothing moves.
Your own inspector
inspector="none" with selectedId and onSelectedIdChange: the app shows the step in its own side panel.
On a phone
At 390px the toolbar centers with 44px buttons and the settings open in a sheet that stops at half height.
Step cards on their own
WorkflowNodeCard for previews and template lists: every kind, running, failed, skipped and selected.
Dependencies
Files
- components/ui/workflow-canvas.tsx
FAQ
Is the React Workflow Canvas component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Workflow Canvas?
Run npx wingo-ui@latest add workflow-canvas. It also adds the Wingo UI items it builds on (Alert Dialog, Button, Drawer, Dropdown Menu, Input and 14 more) and the npm packages @xyflow/react, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/workflow-canvas.
How does Workflow Canvas behave on phones?
On a phone one finger pans and two pinch; tap a step to select it, then drag it (an unselected step never moves by accident); long-press a step for its menu or the empty canvas to add a step; handles take 44px of finger reaching away from the card; the toolbar centers above the home indicator with 44px buttons, folding tidy, zoom and lock into a menu.
See also
Used in
More React AI components
Articles using this component
- React Workflow Builder: Triggers, Branches and AI Steps
Build it: screens and apps13 min read