Skip to content
ilinxa/pro-ui

Flow Canvas

alphav0.3.0

Node-and-edge canvas with typed ports, pluggable node renderers, edge and port-type registries, and JSON save and load — built on React Flow.

Category: Data DisplayUpdated: 2026-08-11Created: 2026-05-06Author: ilinxa

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

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/flow-canvas

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/flow-canvas-fixtures

Preview

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.

CanvasData · JSON
valid

70 lines · valid

Live preview

Nothing rendered yet

Edit the JSON on the left, then press Submit to render the live result on the right.

Demo source

demo.tsxtsx

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).
  • portTypestype id → color/icon/label. Built-ins: data, text, image,card, event (mapped to design tokens).
  • edgeTypestype 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. Backspace AND Delete remove 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. Plug onBeforeConnect for semantic validation on top.
  • Drop / paste pipeline — drag any .json file from desktop, drag JSON from another draggable, or paste with Cmd/Ctrl-V while the canvas is focused. Three MIMEs accepted: application/json, application/reactflow, text/plain. onBeforeDrop intercepts. 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 + an onDragStart calling emitSubObjectDrag. Default copy, Alt-drag for move. Sub-objects without __type auto-coerce to custom-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 in readOnly; consumer items append via menuItems.{canvas, node, edge}.
  • Per-node locknode.locked: true pins position; other ops allowed unless readOnly.
  • Read-only modereadOnly kills drag / connect / mutation menu items; pan / zoom / select / view-only menus stay.
  • Performance — every component memoized; onlyRenderVisibleElements defaults to true in 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. onChange batches mid-drag position-only changes — fires on drag-end, not every tick. Stress demos ship with makeStressData(N) + makeHeavyStressData(N); the sandbox stress page at /sandbox/flow-stress exposes URL params for live N + fixture + lever toggles. Renderer-author perf rules in the procomp guide §8 (linked below).
  • Theming--xy-* CSS variables in globals.css follow 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 / edgeTypes at module scope or via useMemo. 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 data tree, not just the root. Edges reference flat nodeId:portId; the tree-walker resolves location.
  • Never mutate nodes / edges arrays 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-in DefaultEdge follows this rule; the sealed-folder lib/shallow.ts exports a zero-dep shallow helper for selectors returning object shapes. Pattern + the locked portEqual example 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.onEditRequest slot (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 isValidConnection overrides (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.

Tags

flow-canvasflowcanvasnode-editorgraphworkflowxyflowreact-flowportsedges

Dependencies

shadcn primitives: button, context-menu, dialog, textarea
npm peer deps: @xyflow/react@^12.10.2, lucide-react@^1.11.0