Flow Diagram
Interactive flow diagram with nodes, edges, and controls. Install Flow Diagram from the VLLNT UI registry with the shadcn CLI.
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.jsonSource
"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.