react-wireflow
React components for building node-based workflow editors with TypeScript support.

Demo: https://trkbt10.github.io/react-wireflow/
Type-safe node definitions, customizable renderers, grid-based layouts, settings persistence, undo/redo, i18n.
Installation
Requires React ^19.2.0 (uses useEffectEvent).
npm install react-wireflow
import "react-wireflow/style.css";
Usage
import { NodeEditor, createNodeDefinition } from "react-wireflow";
const MyNode = createNodeDefinition({
type: "my-node",
displayName: "My Node",
ports: [
{ id: "in", type: "input", position: "left" },
{ id: "out", type: "output", position: "right" },
],
});
function App() {
const [data, setData] = useState({ nodes: {}, connections: {} });
return <NodeEditor data={data} onDataChange={setData} nodeDefinitions={[MyNode]} />;
}
Changelog
Unreleased
- Breaking:
NodeRenderProps was removed. Use NodeRendererProps instead.
Custom ports and connections
Declare renderPort per port definition to override the visual while keeping editor interactions. The second argument renders the default dot, which you can keep for accessibility hitboxes or replace entirely. Always forward context.handlers and honor context.position (x, y, transform) for correct anchoring.
const CustomPorts = createNodeDefinition({
type: "custom-ports",
displayName: "Custom Ports",
ports: [
{
id: "emit",
type: "output",
label: "Emit",
position: "right",
dataType: ["text", "html"],
renderPort: (context, defaultRender) => {
if (!context.position) return defaultRender();
const { x, y, transform } = context.position;
return (
<div
style={{ position: "absolute", left: x, top: y, transform: transform ?? "translate(-50%, -50%)" }}
onPointerDown={context.handlers.onPointerDown}
onPointerUp={context.handlers.onPointerUp}
onPointerEnter={context.handlers.onPointerEnter}
onPointerMove={context.handlers.onPointerMove}
onPointerLeave={context.handlers.onPointerLeave}
onPointerCancel={context.handlers.onPointerCancel}
data-state={context.isConnectable ? "ready" : context.isHovered ? "hovered" : "idle"}
>
<span className="port-dot" />
<span className="port-label">{context.port.label}</span>
</div>
);
},
renderConnection: (context, defaultRender) => {
if (!context.connection) return defaultRender();
return defaultRender();
},
},
],
});
PortRenderContext includes port, node, allNodes, allConnections, booleans (isConnecting, isConnectable, isCandidate, isHovered, isConnected), optional position, and pointer handlers you must preserve. ConnectionRenderContext provides phase, fromPort, toPort, their positions, selection/hover flags, and handlers for pointer/cxtmenu; use it to add badges or halos while keeping hit-testing intact. For dynamic ports, set instances, createPortId, and createPortLabel on the port definition (see src/examples/demos/custom/ports/port-playground for a complete playground).
Panels
Use defaultEditorGridLayers for built-in panels (canvas, inspector, statusbar):
import { defaultEditorGridConfig, defaultEditorGridLayers } from "react-wireflow";
<NodeEditor gridConfig={defaultEditorGridConfig} gridLayers={defaultEditorGridLayers} />
Or define custom layouts:
<NodeEditor
gridConfig={{
areas: [["canvas", "inspector"]],
rows: [{ size: "1fr" }],
columns: [{ size: "1fr" }, { size: "300px", resizable: true }],
}}
gridLayers={[
{ id: "canvas", component: <NodeCanvas />, gridArea: "canvas" },
{ id: "inspector", component: <InspectorPanel />, gridArea: "inspector" },
]}
/>
Add floating layers:
const layers = [
...defaultEditorGridLayers,
{
id: "minimap",
component: <YourMinimap />,
positionMode: "absolute",
position: { right: 10, bottom: 10 },
draggable: true,
},
];
Drawer for mobile:
{ id: "panel", component: <MyPanel />, drawer: { placement: "right", open: isOpen } }
See examples for complete implementations.
Custom Inspector Panels
The Inspector panel can be customized at three levels:
1. Per-Node Custom Inspector (renderInspector)
Define renderInspector in your node definition to provide custom inspector content when that node type is selected:
import {
createNodeDefinition,
PropertySection,
InspectorInput,
InspectorDefinitionList,
InspectorDefinitionItem,
type InspectorRenderProps,
} from "react-wireflow";
type PersonNodeData = {
name: string;
email: string;
};
function PersonInspector({ node, onUpdateNode }: InspectorRenderProps<PersonNodeData>) {
const data = node.data ?? {};
return (
<PropertySection title="Person Details">
<InspectorDefinitionList>
<InspectorDefinitionItem label="Name">
<InspectorInput
value={data.name ?? ""}
onChange={(e) => onUpdateNode({ data: { ...data, name: e.target.value } })}
/>
</InspectorDefinitionItem>
<InspectorDefinitionItem label="Email">
<InspectorInput
type="email"
value={data.email ?? ""}
onChange={(e) => onUpdateNode({ data: { ...data, email: e.target.value } })}
/>
</InspectorDefinitionItem>
</InspectorDefinitionList>
</PropertySection>
);
}
const PersonNode = createNodeDefinition({
type: "person",
displayName: "Person",
renderInspector: PersonInspector,
});
InspectorRenderProps provides:
node - The selected node with typed data
onUpdateNode(updates) - Callback to update node properties
onDeleteNode() - Callback to delete the node
externalData, isLoadingExternalData, externalDataError - External data state
onUpdateExternalData(data) - Callback to update external data
2. Custom Inspector Tabs
Replace or extend the default tabs (Layers, Properties, Settings) by passing tabs to InspectorPanel:
import {
InspectorPanel,
InspectorLayersTab,
InspectorPropertiesTab,
InspectorSettingsTab,
InspectorSection,
PropertySection,
type InspectorPanelTabConfig,
} from "react-wireflow";
const StatisticsTab = () => (
<InspectorSection>
<PropertySection title="Statistics">
<p>Total nodes: 10</p>
</PropertySection>
</InspectorSection>
);
const customTabs: InspectorPanelTabConfig[] = [
{ id: "layers", label: "Layers", render: () => <InspectorLayersTab /> },
{ id: "properties", label: "Properties", render: () => <InspectorPropertiesTab /> },
{ id: "stats", label: "Stats", render: () => <StatisticsTab /> },
{ id: "settings", label: "Settings", render: () => <InspectorSettingsTab panels={[]} /> },
];
<InspectorPanel tabs={customTabs} />
3. Custom Settings Panels
Add panels to the Settings tab using settingsPanels:
import {
InspectorPanel,
InspectorDefinitionList,
InspectorDefinitionItem,
InspectorButton,
type InspectorSettingsPanelConfig,
} from "react-wireflow";
const ExportPanel = () => {
return (
<InspectorDefinitionList>
<InspectorDefinitionItem label="Format">
<select><option>JSON</option><option>YAML</option></select>
</InspectorDefinitionItem>
<InspectorDefinitionItem label="">
<InspectorButton onClick={() => alert("Exporting...")}>Export</InspectorButton>
</InspectorDefinitionItem>
</InspectorDefinitionList>
);
};
const settingsPanels: InspectorSettingsPanelConfig[] = [
{ title: "Export Options", component: ExportPanel },
];
<InspectorPanel settingsPanels={settingsPanels} />
Available Inspector UI Components
Build consistent inspector UIs with these components:
Layout Components:
PropertySection | Titled section with header |
InspectorSection | Basic section container |
InspectorSectionTitle | Standalone section title (H4) |
InspectorDefinitionList | Semantic <dl> wrapper |
InspectorDefinitionItem | Label-value pair (<dt>/<dd>) |
InspectorField | Field wrapper with label |
PositionInputsGrid | Grid layout for position/size inputs |
Form Inputs:
InspectorInput | Styled text input |
InspectorNumberInput | Number input with label |
InspectorTextarea | Multi-line text input |
InspectorSelect | Styled select dropdown |
InspectorLabel | Standalone form label |
ReadOnlyField | Non-editable display field |
Interactive:
InspectorButton | Button (variants: primary, secondary, danger) |
InspectorButtonGroup | Segmented button control for options |
InspectorShortcutButton | Compact button for shortcut settings |
InspectorShortcutBindingValue | Keyboard/pointer shortcut display |
See the Custom Inspector example for a complete implementation.