It is part of the ts-graphviz library, which is split into modular packages to improve maintainability, flexibility, and ease of use.
The module can then be installed using npm:
# yarn
$ yarn add @ts-graphviz/react react@^19
# or npm
$ npm install -S @ts-graphviz/react react@^19
# or pnpm
$ pnpm add @ts-graphviz/react react@^19
Important: Install React 19+ as a peerDependency. React 18 and earlier versions are not supported. Note that
react-domis no longer required as this package now uses a custom HTML rendering implementation.
The package provides React components that map directly to Graphviz DOT language constructs:
<Digraph> - Creates a directed graph (arrows point from source to target)<Graph> - Creates an undirected graph (no arrow direction)<Subgraph> - Creates a subgraph cluster for grouping related nodes<Node> - Creates a node/vertex with customizable attributes<Edge> - Creates an edge/connection between nodes with optional stylingAll components accept Graphviz DOT attributes as props with full TypeScript support:
// Node with styling attributes
<Node
id="server1"
label="Web Server"
shape="box"
color="blue"
style="filled"
fillcolor="lightblue"
/>
// Edge with custom styling
<Edge
targets={["server1", "database1"]}
label="API calls"
color="red"
style="dashed"
weight={2}
/>
// Digraph with global attributes
<Digraph
rankdir="LR"
bgcolor="white"
node={{ shape: "ellipse", color: "gray" }}
edge={{ color: "black", arrowhead: "vee" }}
>
{/* nodes and edges */}
</Digraph>
Components can be freely nested to create complex graph structures:
// Define reusable components
const ServerNode = ({ id, name, type }) => (
<Node
id={id}
label={
<dot:table border="1" cellborder="0" cellspacing="0">
<dot:tr>
<dot:td bgcolor="lightgray">
<dot:b>{name}</dot:b>
</dot:td>
</dot:tr>
<dot:tr>
<dot:td>{type}</dot:td>
</dot:tr>
</dot:table>
}
shape="record"
/>
);
const ServiceCluster = ({ id, label, children }) => (
<Subgraph id={id} label={label} style="filled" fillcolor="lightblue">
{children}
</Subgraph>
);
// Compose the architecture
const SystemArchitecture = () => (
<Digraph rankdir="TB">
<ServiceCluster id="cluster_frontend" label="Frontend Layer">
<ServerNode id="web1" name="Web Server" type="Nginx" />
<ServerNode id="app1" name="App Server" type="React" />
</ServiceCluster>
<ServiceCluster id="cluster_backend" label="Backend Layer">
<ServerNode id="api1" name="API Gateway" type="REST" />
<ServerNode id="auth1" name="Auth Service" type="OAuth" />
</ServiceCluster>
<ServiceCluster id="cluster_data" label="Data Layer">
<ServerNode id="db1" name="Database" type="PostgreSQL" />
<ServerNode id="cache1" name="Cache" type="Redis" />
</ServiceCluster>
<Edge targets={["web1", "app1"]} label="serves" />
<Edge targets={["app1", "api1"]} label="API calls" />
<Edge targets={["api1", "auth1"]} label="validates" />
<Edge targets={["api1", "db1"]} label="queries" />
<Edge targets={["api1", "cache1"]} label="caches" />
</Digraph>
);
Create rich, formatted labels using Graphviz's HTML-like label syntax with natural JSX:
// Reusable table component for data records
const DataRecord = ({ title, fields }) => (
<dot:table border="1" cellborder="0" cellspacing="0">
<dot:tr>
<dot:td bgcolor="lightblue" colspan="2">
<dot:b>{title}</dot:b>
</dot:td>
</dot:tr>
{fields.map(([key, value]) => (
<dot:tr key={key}>
<dot:td port={key}>{key}</dot:td>
<dot:td>{value}</dot:td>
</dot:tr>
))}
</dot:table>
);
// Status indicator component
const StatusIndicator = ({ status, message }) => (
<>
<dot:b>Status:</dot:b><dot:br/>
<dot:font color={status === 'active' ? 'green' : 'red'}>
<dot:b>{status.toUpperCase()}</dot:b>
</dot:font><dot:br/>
<dot:i>{message}</dot:i>
</>
);
// Usage in graph
const DatabaseDiagram = () => (
<Digraph>
<Node
id="user-table"
label={<DataRecord
title="Users"
fields={[['id', '1001'], ['name', 'John Doe'], ['email', 'john@example.com']]}
/>}
shape="record"
/>
<Node
id="server-status"
label={<StatusIndicator status="active" message="All systems operational" />}
shape="box"
style="rounded,filled"
fillcolor="lightyellow"
/>
<Edge targets={["user-table", "server-status"]} label="monitors" />
</Digraph>
);
<dot:table>, <dot:tr>, <dot:td> - Table structures<dot:b>, <dot:i>, <dot:u> - Text formatting (bold, italic, underline)<dot:font> - Font styling with color, face, point-size<dot:br> - Line breaks<dot:hr>, <dot:vr> - Horizontal/vertical rules<dot:img> - Images<dot:s>, <dot:sub>, <dot:sup>, <dot:o> - Advanced text formattingFull type safety with IntelliSense support for all Graphviz attributes:
import type { NodeProps, EdgeProps, DigraphProps } from '@ts-graphviz/react';
// Typed component props
const MyNode: React.FC<NodeProps> = (props) => (
<Node
shape="box" // ✅ TypeScript knows valid shapes
color="blue" // ✅ String colors supported
style="filled" // ✅ Valid style options
{...props}
/>
);
// Edge with typed targets
<Edge
targets={["node1", "node2"]} // ✅ Tuple type enforced
arrowhead="diamond" // ✅ Valid arrowhead styles
/>
TypeScript type definitions for all dot:* HTML-like elements are automatically available when you import @ts-graphviz/react. No additional configuration or setup is required.
import { Digraph, Node, Edge } from '@ts-graphviz/react';
// Full IntelliSense and type checking for dot:* elements works out of the box
<Node
id="user"
label={
<dot:table border={1}>
<dot:tr>
<dot:td>Name</dot:td>
<dot:td>John Doe</dot:td>
</dot:tr>
</dot:table>
}
shape="box"
/>
The DotJSXElements type is also exported for advanced use cases where you need to reference the type definitions directly.
The package provides sophisticated TypeScript support with automatic type inference and runtime type filtering:
// ✅ Automatic type inference - no casting needed
const root = createRoot();
await root.render(
<Digraph id="myGraph" rankdir="LR">
<Node id="A" shape="box" />
<Node id="B" shape="circle" />
<Edge targets={["A", "B"]} />
</Digraph>
);
// ✅ Type-safe model access
const models = root.getTopLevelModels();
// models is automatically typed as DotObjectModel[]
// ✅ Runtime type filtering with built-in type guards
import { isNodeModel, isEdgeModel, isRootGraphModel } from '@ts-graphviz/common';
// Filter by model type with automatic type narrowing
const nodes = root.getTopLevelModels(isNodeModel);
nodes.forEach(node => console.log(node.id)); // TypeScript knows this is NodeModel
const edges = root.getTopLevelModels(isEdgeModel);
edges.forEach(edge => console.log(edge.targets)); // TypeScript knows this is EdgeModel
const graphs = root.getTopLevelModels(isRootGraphModel);
graphs.forEach(graph => console.log(graph.directed)); // TypeScript knows this is RootGraphModel
// ✅ Direct type casting (trusted user assertion)
// When you know the exact types, you can cast directly without runtime validation
const trustedNodes = root.getTopLevelModels<NodeModel>();
trustedNodes.forEach(node => console.log(node.id)); // TypeScript trusts your assertion
const trustedEdges = root.getTopLevelModels<EdgeModel>();
trustedEdges.forEach(edge => console.log(edge.targets)); // No runtime type checking
// ✅ Advanced model type checking
const allModels = root.getTopLevelModels();
for (const model of allModels) {
if (isNodeModel(model)) {
console.log(`Node: ${model.id}`);
} else if (isEdgeModel(model)) {
console.log(`Edge: ${model.targets.map(t => t.id).join(' -> ')}`);
} else if (isRootGraphModel(model)) {
console.log(`Graph: ${model.id} (directed: ${model.directed})`);
}
}
When using container mode, you get access to all rendered models with full type safety:
import { digraph } from 'ts-graphviz';
import { isNodeModel, isEdgeModel, isSubgraphModel } from '@ts-graphviz/react';
const container = digraph('myContainer');
const root = createRoot({ container });
await root.render(
<>
<Node id="node1" />
<Node id="node2" />
<Edge targets={['node1', 'node2']} />
<Subgraph id="cluster1">
<Node id="node3" />
</Subgraph>
</>
);
// Container mode: access all non-container models with type safety
// Runtime type filtering (safe, validates at runtime)
const allNodes = root.getTopLevelModels(isNodeModel); // NodeModel[]
const allEdges = root.getTopLevelModels(isEdgeModel); // EdgeModel[]
const subgraphs = root.getTopLevelModels(isSubgraphModel); // SubgraphModel[]
// Direct type casting (user knows the types, no runtime validation)
const trustedNodes = root.getTopLevelModels<NodeModel>(); // All models cast as NodeModel[]
const trustedEdges = root.getTopLevelModels<EdgeModel>(); // All models cast as EdgeModel[]
// Type-safe operations
allNodes.forEach(node => {
node.attributes.set('color', 'blue'); // TypeScript knows node attributes
});
allEdges.forEach(edge => {
console.log(`Edge from ${edge.targets[0]} to ${edge.targets[1]}`);
});
The package provides clean async rendering APIs:
createRoot() - Creates a rendering root following React 19's createRoot patternrenderToDot() - Primary async function for converting React components to DOT language stringsrenderHTMLLike() - Renders HTML-like label structures for use in node or edge labelsAll rendering functions are async-only and provide a clean, consistent API surface. The new createRoot() API follows React 19's modern patterns for better performance and error handling.
import { Digraph, Node, Edge, createRoot, renderToDot } from "@ts-graphviz/react";
// Define a reusable process component
const ProcessNode = ({ id, label, color = "lightblue" }) => (
<Node
id={id}
label={
<dot:table border="0" cellborder="1" cellspacing="0">
<dot:tr>
<dot:td bgcolor={color}>
<dot:b>{label}</dot:b>
</dot:td>
</dot:tr>
</dot:table>
}
shape="record"
/>
);
// Create a workflow diagram
const WorkflowDiagram = () => (
<Digraph rankdir="LR">
<ProcessNode id="start" label="Start" color="lightgreen" />
<ProcessNode id="process" label="Process Data" />
<ProcessNode id="validate" label="Validate" />
<ProcessNode id="end" label="End" color="lightcoral" />
<Edge targets={["start", "process"]} />
<Edge targets={["process", "validate"]} />
<Edge targets={["validate", "end"]} />
</Digraph>
);
// Create root and render to graph models
const root = createRoot();
await root.render(<WorkflowDiagram />);
const models = root.getTopLevelModels();
// Convert to DOT string
const dotString = await renderToDot(<WorkflowDiagram />);
import { Digraph, Node, Edge, renderToDot } from "@ts-graphviz/react";
// Reusable card component with HTML-like labels
const InfoCard = ({ id, title, items }) => (
<Node
id={id}
label={
<dot:table border="1" cellborder="0" cellspacing="0">
<dot:tr>
<dot:td bgcolor="navy">
<dot:font color="white">
<dot:b>{title}</dot:b>
</dot:font>
</dot:td>
</dot:tr>
{items.map((item, index) => (
<dot:tr key={index}>
<dot:td align="left">• {item}</dot:td>
</dot:tr>
))}
</dot:table>
}
shape="record"
/>
);
// Usage in graph
const ProjectDiagram = () => (
<Digraph>
<InfoCard
id="requirements"
title="Requirements"
items={["User login", "Data processing", "Reporting"]}
/>
<InfoCard
id="implementation"
title="Implementation"
items={["React frontend", "Node.js API", "PostgreSQL DB"]}
/>
<Edge targets={["requirements", "implementation"]} label="leads to" />
</Digraph>
);
const dotString = await renderToDot(<ProjectDiagram />);
The renderHTMLLike function converts React elements with HTML-like labels into Graphviz-compatible markup. It includes built-in security protections against deeply nested structures and circular references.
import { renderHTMLLike } from "@ts-graphviz/react";
// Basic usage
const htmlLabel = renderHTMLLike(
<dot:table>
<dot:tr>
<dot:td>left</dot:td>
<dot:td>right</dot:td>
</dot:tr>
</dot:table>
);
// With custom security options
const deeplyNestedLabel = renderHTMLLike(
<dot:table>
{/* ... deeply nested structure ... */}
</dot:table>,
{ maxDepth: 2000 } // Increase limit for complex structures
);
// Stricter validation for user-generated content
const userContent = renderHTMLLike(
userProvidedElement,
{ maxDepth: 100 } // Lower limit for untrusted input
);
Important: The HTML-like labels generated by this library are not browser HTML. They are:
XSS Risk Context:
@ts-graphviz/adapter documentation for handling user-provided DOT files safelyThe renderHTMLLike function provides security protections against stack overflow attacks:
maxDepth (default: 1000): Maximum recursion depth for nested elements
maxDepth: 5000)maxDepth: 100)'<>')import { renderHTMLLike, type RenderHTMLLikeOptions } from "@ts-graphviz/react";
// Type-safe options
const options: RenderHTMLLikeOptions = {
maxDepth: 1500
};
const result = renderHTMLLike(<dot:table>...</dot:table>, options);
import { Digraph, Node, Edge, createRoot } from "@ts-graphviz/react";
// Basic usage
const root = createRoot();
await root.render(
<Digraph>
<Node id="A" />
<Node id="B" />
<Edge targets={["A", "B"]} />
</Digraph>
);
// Container mode - render into existing graph
import { digraph } from 'ts-graphviz';
const container = digraph('MyGraph');
const containerRoot = createRoot({ container });
await containerRoot.render(
<>
<Node id="A" />
<Node id="B" />
<Edge targets={["A", "B"]} />
</>
);
// Error handling options
const rootWithErrorHandling = createRoot({
onUncaughtError: (error, errorInfo) => {
console.error('Rendering error:', error);
console.log('Component stack:', errorInfo.componentStack);
},
onCaughtError: (error, errorInfo) => {
console.error('Caught error:', error);
}
});
await rootWithErrorHandling.render(<MyComplexGraph />);
The package provides robust error handling capabilities for rendering errors:
import { createRoot, renderToDot } from "@ts-graphviz/react";
// Error handling with createRoot
const root = createRoot({
onUncaughtError: (error, errorInfo) => {
console.error('Uncaught rendering error:', error.message);
console.log('Component stack:', errorInfo.componentStack);
// Send to error tracking service
errorTracker.captureException(error, { extra: errorInfo });
},
onCaughtError: (error, errorInfo) => {
console.error('Caught by error boundary:', error.message);
// Handle recoverable errors
}
});
await root.render(<MyGraph />);
// renderToDot also supports error handling
try {
const dotString = await renderToDot(<ComplexGraph />);
} catch (error) {
if (error.message.includes('Multiple top-level graphs')) {
console.error('Invalid graph structure');
}
}
The package supports using ref to access and manipulate graph models directly, allowing for dynamic updates and interactions:
import { useRef } from 'react';
import { Digraph, Graph, Node, Edge, createRoot } from "@ts-graphviz/react";
import type { NodeModel, EdgeModel, GraphBaseModel } from 'ts-graphviz';
function MyGraphComponent() {
const nodeRef = useRef<NodeModel>(null);
const edgeRef = useRef<EdgeModel>(null);
const digraphRef = useRef<GraphBaseModel>(null);
const graphRef = useRef<GraphBaseModel>(null);
const handleRender = async () => {
// Example with Digraph component
const digraphRoot = createRoot();
await digraphRoot.render(
<Digraph id="mygraph" ref={digraphRef}>
<Node id="A" ref={nodeRef} label="Node A" />
<Node id="B" label="Node B" />
<Edge targets={['A', 'B']} ref={edgeRef} label="A to B" />
</Digraph>
);
// Example with Graph component (undirected)
const graphRoot = createRoot();
await graphRoot.render(
<Graph id="undirected-graph" ref={graphRef}>
<Node id="X" label="Node X" />
<Node id="Y" label="Node Y" />
<Edge targets={['X', 'Y']} label="X -- Y" />
</Graph>
);
// Access and manipulate the models directly
if (nodeRef.current) {
nodeRef.current.attributes.set('color', 'red');
nodeRef.current.comment = 'Modified via ref';
}
if (edgeRef.current) {
edgeRef.current.attributes.set('style', 'dashed');
}
console.log('Digraph nodes:', digraphRef.current?.nodes.length);
console.log('Digraph edges:', digraphRef.current?.edges.length);
console.log('Graph nodes:', graphRef.current?.nodes.length);
console.log('Graph edges:', graphRef.current?.edges.length);
};
return (
<button onClick={handleRender}>
Render Graph
</button>
);
}
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
This software is released under the MIT License, see LICENSE.