Flow Diagram

Interactive flow diagram with nodes, edges, and controls. Install Flow Diagram from the VLLNT UI registry with the shadcn CLI.

Report a bug

Interactive flow diagram with nodes, edges, and controls. Part of the data family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

Add Flow Diagram to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:

pnpm dlx shadcn@latest add https://ui.vllnt.com/r/flow-diagram.json

Source

"use client"; import "@xyflow/react/dist/style.css"; import { memo, useCallback, useEffect } from "react"; import { type Node, type NodeMouseHandler, ReactFlowProvider, } from "@xyflow/react"; import { cn } from "../../lib/utils"; import { FlowCanvas } from "./flow-canvas"; import { FlowErrorBoundary } from "./flow-error-boundary"; import { FlowFullscreen } from "./flow-fullscreen"; import type { FlowDiagramEdge, FlowDiagramNode, FlowDiagramProps, } from "./types"; import { useFlowDiagram } from "./use-flow-diagram"; /** * Validates nodes and edges, logging warnings for common issues. */ function validateFlowData( nodes: FlowDiagramNode[], edges: FlowDiagramEdge[], ): void { if (nodes.length === 0 && edges.length > 0) { console.warn( "[FlowDiagram] Edges provided without nodes - edges will not render", ); } const nodeIds = new Set(nodes.map((n) => n.id)); const invalidEdges = edges.filter( (e) => !nodeIds.has(e.source) || !nodeIds.has(e.target), ); if (invalidEdges.length > 0) { console.warn( `[FlowDiagram] ${invalidEdges.length} edge(s) reference non-existent nodes:`, invalidEdges.map((e) => `${e.id}: ${e.source} -> ${e.target}`), ); } const nodesWithoutPosition = nodes.filter((n) => n.position === undefined); if (nodesWithoutPosition.length > 0) { console.warn( `[FlowDiagram] ${nodesWithoutPosition.length} node(s) missing position:`, nodesWithoutPosition.map((n) => n.id), ); } } const FlowDiagramInner = memo(function FlowDiagramInner({ allowCopy = false, allowFullscreen = true, className, edges, fitView = true, fitViewOptions, height = 400, nodes, onNodeClick, showControls = true, title, }: FlowDiagramProps) { // Check input on mount and when data changes useEffect(() => { validateFlowData(nodes, edges); }, [nodes, edges]); const { closeFullscreen, copyStatus, copyToClipboard, fitView: handleFitView, isFullscreen, toggleFullscreen, zoomIn, zoomOut, } = useFlowDiagram({ allowCopy, allowFullscreen }); // Memoize node click handler to prevent unnecessary re-renders const handleNodeClick: NodeMouseHandler | undefined = useCallback( (_event: React.MouseEvent, node: Node) => { onNodeClick?.(node as FlowDiagramNode); }, [onNodeClick], ); const handleCopy = useCallback(() => { void copyToClipboard(); }, [copyToClipboard]); const canvasProps = { allowCopy, allowFullscreen, className, copyStatus, edges, fitView, fitViewOptions, height, isFullscreen, nodes, onCopy: allowCopy ? handleCopy : undefined, onFitView: handleFitView, onFullscreen: allowFullscreen ? toggleFullscreen : undefined, onNodeClick: onNodeClick ? handleNodeClick : undefined, onZoomIn: zoomIn, onZoomOut: zoomOut, showControls, title, }; if (isFullscreen) { return ( <> <div className={cn( "rounded-lg border border-border bg-muted/50", className, )} style={{ height }} /> <FlowFullscreen isOpen={isFullscreen} onClose={closeFullscreen}> <FlowCanvas {...canvasProps} /> </FlowFullscreen> </> ); } return <FlowCanvas {...canvasProps} />; }); /** * FlowDiagram component for rendering interactive flow diagrams. * Uses @xyflow/react under the hood with error boundary protection. * * @example * ```tsx * <FlowDiagram * nodes={[ * { id: '1', data: { label: 'Start' }, position: { x: 0, y: 0 } }, * { id: '2', data: { label: 'End' }, position: { x: 200, y: 100 } } * ]} * edges={[{ id: 'e1-2', source: '1', target: '2' }]} * showControls * allowFullscreen * /> * ``` */ export const FlowDiagram = memo(function FlowDiagram(props: FlowDiagramProps) { return ( <FlowErrorBoundary height={props.height}> <ReactFlowProvider> <FlowDiagramInner {...props} /> </ReactFlowProvider> </FlowErrorBoundary> ); });

Stories

Explore every variant and state in the interactive Storybook:

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Dependencies

  • @vllnt/ui@^0.3.0