Flow Canvas
alphav0.3.0Node-and-edge canvas with typed ports, pluggable node renderers, edge and port-type registries, and JSON save and load — built on React Flow.
Context
Use FlowCanvas to build flow editors, workflow canvases, AI agent graphs, visual configuration UIs, or any port-and-edge surface where nodes should be JSON-first. Every node carries a __type field that keys into a renderer registry; unknown shapes fall back to a built-in custom-JSON renderer. Connection ports live inside the data, recursively, so sub-objects can be extracted as standalone nodes through the same drop pipeline. The library is xyflow (formerly React Flow) — MIT-licensed, no feature gates.
Installation
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/flow-canvasAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/flow-canvas-fixturesPreview
Five renderers — Prompt, LLM (with extractable tool chips), Display, Project card adapter, and the custom-JSON fallback. Drag handles to connect; the LLM's output has multi: true so it fans out. Drag a tool chip onto empty canvas to extract it as a node; Alt-drag to move.
Live playground
flow-canvas renders a node graph from a JSON CanvasData (nodes + edges). Nodes use a card renderer or fall back to the built-in custom-JSON node; edges connect nodeId:portId refs. Submit to render the live, interactive canvas.
70 lines · valid
Nothing rendered yet
Edit the JSON on the left, then press Submit to render the live result on the right.
Demo source
Usage
When to use
FlowCanvas is a node-and-edge canvas built on @xyflow/react. Reach for it when you need a flow editor, workflow canvas, AI agent graph, schema designer, or any port-and-edge UI where nodes are data-first — JSON objects discriminated by a __type field that keys into a renderer registry.
When NOT to use
- Force-directed graph layouts (use
force-graph-01). - Hierarchical org / tree visualizations — flow-canvas is free-form; tree layouts want a tree component.
- Pure timelines, Gantt charts, dense data tables, kanban boards — each has a dedicated component in the registry.
Three keystone registries
Every "what does this look like" decision flows through one of three consumer-extendable registries — built-ins ship, consumers append:
renderers—__type → React renderer. Built-in:customJsonRenderer(the fallback for unknown shapes).portTypes—type id → color/icon/label. Built-ins:data,text,image,card,event(mapped to design tokens).edgeTypes—type id → React edge renderer. Built-in:defaultEdgeRenderer(smoothstep, stroke pulled from source-port type color). Consumer-registered edge dispatch ships in v0.2.
Basic example
import { FlowCanvas, type NodeRenderer } from "@/components/flow-canvas"
// Register a renderer for your domain type. Define at module scope (or
// useMemo) — recreating the array on every render thrashes xyflow's
// nodeTypes registry.
const promptRenderer: NodeRenderer = {
type: "prompt",
label: "Prompt",
defaultPorts: () => [
{ id: "out", side: "right", dir: "out", type: "text" },
],
render: (data) => <PromptCard data={data} />,
}
const RENDERERS = [promptRenderer]
export function MyEditor() {
return (
<div className="h-screen w-full"> {/* parent MUST have explicit dims */}
<FlowCanvas
renderers={RENDERERS}
defaultData={{ version: 1, nodes: [], edges: [] }}
onChange={(next) => persist(next)}
/>
</div>
)
}Save / restore via exportRef
import { useRef } from "react"
import { type FlowCanvasExportHandle } from "@/components/flow-canvas"
const exportRef = useRef<FlowCanvasExportHandle>(null)
// Round-trip with ports + edges (canvas-instance state):
const portable = exportRef.current?.export({ withPorts: true })
// Strip ports + edges for source-shape persistence:
const sourceShape = exportRef.current?.export({ withPorts: false })Capabilities — v0.1.0
- Pan / zoom / fit-view / select / delete — pointer + keyboard.
BackspaceANDDeleteremove selected nodes/edges (cascades). - Typed connections — drag-to-connect; mismatched-type / wrong-direction /
multi: false-already-connected pairs rejected with the in-flight rejection indicator. PlugonBeforeConnectfor semantic validation on top. - Drop / paste pipeline — drag any
.jsonfile from desktop, drag JSON from another draggable, or paste withCmd/Ctrl-Vwhile the canvas is focused. Three MIMEs accepted:application/json,application/reactflow,text/plain.onBeforeDropintercepts. Default ports inflate at the drop boundary;ports: [](deliberate empty) is respected. - Sub-object drag-extract — renderers mark draggable sub-paths with
data-draggable-subobject={path}+draggable+ anonDragStartcallingemitSubObjectDrag. Default copy, Alt-drag for move. Sub-objects without__typeauto-coerce tocustom-json. - Right-click menus — three contexts (canvas, node, edge). Built-ins: Paste JSON…, Add custom node, Fit view, Reset zoom, Copy as JSON, Duplicate, Convert to custom-JSON, Extract
<path>(keyboard fallback for sub-object extract), Delete. Mutation items hide inreadOnly; consumer items append viamenuItems.{canvas, node, edge}. - Per-node lock —
node.locked: truepins position; other ops allowed unlessreadOnly. - Read-only mode —
readOnlykills drag / connect / mutation menu items; pan / zoom / select / view-only menus stay. - Performance — every component memoized;
onlyRenderVisibleElementsdefaults totruein v0.2.0 (xyflow culls offscreen nodes / edges; ~12× directional FPS lift at N=5000 heavy on one measured machine). Pass={false}to opt out — rare; only if you rely on offscreen-node DOM.onChangebatches mid-drag position-only changes — fires on drag-end, not every tick. Stress demos ship withmakeStressData(N)+makeHeavyStressData(N); the sandbox stress page at/sandbox/flow-stressexposes URL params for live N + fixture + lever toggles. Renderer-author perf rules in the procomp guide §8 (linked below). - Theming —
--xy-*CSS variables inglobals.cssfollow design tokens (signal-lime accent on ring + selected edges; OKLCH palette for handles). Light + dark via class-based toggle on a parent.
Critical rules
- Parent must have explicit width AND height. Putting
h-screenon the canvas itself doesn't work — xyflow measures its parent. - Define
renderers/portTypes/edgeTypesat module scope or viauseMemo. Recreating the arrays each render triggers teardown + remount of every node — flicker, lost focus, sometimes infinite loops. (We mitigate internally by routing all nodes through a single"ilinxa-node"xyflow type, but the rule still matters for the props you pass us.) - Port IDs must be unique within a node — across the entire
datatree, not just the root. Edges reference flatnodeId:portId; the tree-walker resolves location. - Never mutate
nodes/edgesarrays in place. Always spread:{ ...node, data: { ...node.data, x } }. Mutation breaks change detection. - Single source of truth for state. Pick controlled (
data+onChange) or uncontrolled (defaultData) per<FlowCanvas>instance. Don't flip mid-mount.
Performance & scale — renderer-author rules (v0.2.0)
- Custom edge selection state:if your custom edge needs to react to source / target node selection, query xyflow's store via
useStore— NOT per-edge React state. Per-edge state cascades into full-canvas re-renders at scale. The built-inDefaultEdgefollows this rule; the sealed-folderlib/shallow.tsexports a zero-depshallowhelper for selectors returning object shapes. Pattern + the lockedportEqualexample in procomp guide §8.2. - Popup-edit convention: heavy editable content (rich-text, code editors, multi-line forms) lives in a consumer-owned dialog opened on click, NOT inline in the node renderer. Inline editors mount the full state machine per node (e.g. 200 editor instances at N=200) — putting the editor in a single dialog scopes that cost to the one node being edited. Pattern in procomp guide §8.3 + the future
RenderContext.onEditRequestslot (v0.3 candidate).
Deferred to v0.3+ (Tier 3 + adjacent)
- Custom edge renderers dispatched per-edge via
edgeTypes(registry exists; dispatch is Tier 3 per perf description) - Per-handle
isValidConnectionoverrides (node-local connection-validation slot)
Future considerations (no scheduled version)
- Toast notifications on parse error (today:
console.warn+ silent abort) - Undo / redo, marquee selection, groups / frames, minimap, execution-state animation
- DB-ref nodes (
{ ref: 'post:abc123' }placeholders that fetch on demand) - Cross-canvas drag
Full reference: see docs/procomps/flow-canvas-procomp/flow-canvas-procomp-guide.md. Architecture decisions Q1–Q24 live in the description doc; the implementation contract lives in the plan doc.
Features
- Pan / zoom / fit-to-view with gradient background (light + dark themes)
- Renderer registry by __type — register any React component as a node renderer
- Built-in custom-JSON fallback for unrecognized shapes
- Port-type registry (6 built-in types: data / text / image / card / event / doc)
- Edge-type registry with smoothstep default
- Controlled and uncontrolled state (data / defaultData / onChange)
- Imperative export via exportRef ({ withPorts } toggles source vs canvas)
- Typed connection validation, multi-edge per port, sub-object drag-extract (M2+)
- Read-only mode that preserves pan / zoom / select
- Popup-edit renderer convention (v0.2.1) — onEditRequest + updateNodeData helper
- v0.2.7 — F-cross-13 path-b sweep: canvas ContextMenuTrigger drops `asChild` (`className="contents"` wrapper; right-click bubbles children → resolver → trigger in both backends). Zero public-API change.