How to Use Mermaid.js in React — Complete Integration Guide (2026)
Step-by-step guide to integrating Mermaid.js into a React app. Covers hooks, dynamic rendering, dark mode, SSR pitfalls, and a reusable MermaidDiagram component.
# How to Use Mermaid.js in React — Complete Integration Guide (2026)
Mermaid.js renders diagrams from plain text, and React is the most popular UI framework for building web apps. Putting them together seems obvious — but there are a few gotchas that catch most developers the first time. This guide walks you through the cleanest, most production-ready approach.
By the end you'll have a reusable component that handles re-renders, dark mode, and server-side rendering safely.
Installing Mermaid
npm install mermaid
# or
yarn add mermaidMermaid v11 is the current stable release. It ships as an ES module, which works great with modern React toolchains (Vite, Next.js App Router, Create React App).
The Problem with Naive Integration
You might try something like this:
// ❌ Don't do this
import mermaid from 'mermaid';
function BadDiagram({ chart }: { chart: string }) {
mermaid.initialize({ startOnLoad: true });
return <div className="mermaid">{chart}</div>;
}This breaks in several ways:
- initialize runs on every render
- Mermaid tries to find .mermaid elements in the DOM before React has committed them
- On re-renders, old SVGs stack up inside the div
- On Next.js (SSR), it throws because document doesn't exist on the server
Here's the right approach.
A Reusable MermaidDiagram Component
// components/MermaidDiagram.tsx
'use client'; // Next.js App Router only — remove for plain React/Vite
import { useEffect, useRef, useState } from 'react';
import mermaid from 'mermaid';
let initialized = false;
function initMermaid(theme: 'default' | 'dark' | 'neutral' | 'forest' = 'default') {
if (initialized) return;
mermaid.initialize({
startOnLoad: false,
theme,
securityLevel: 'loose',
fontFamily: 'inherit',
});
initialized = true;
}
interface MermaidDiagramProps {
chart: string;
theme?: 'default' | 'dark' | 'neutral' | 'forest';
className?: string;
}
export default function MermaidDiagram({
chart,
theme = 'default',
className = '',
}: MermaidDiagramProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [error, setError] = useState<string | null>(null);
const [svg, setSvg] = useState<string>('');
useEffect(() => {
initMermaid(theme);
let cancelled = false;
async function render() {
try {
const id = 'mermaid-' + Math.random().toString(36).slice(2, 9);
const { svg: renderedSvg } = await mermaid.render(id, chart);
if (!cancelled) {
setSvg(renderedSvg);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : 'Render error');
setSvg('');
}
}
}
render();
return () => {
cancelled = true;
};
}, [chart, theme]);
if (error) {
return (
<div className={`mermaid-error ${className}`} style={{ color: '#ef4444', fontSize: 14 }}>
<strong>Diagram error:</strong> {error}
</div>
);
}
return (
<div
ref={containerRef}
className={`mermaid-container ${className}`}
dangerouslySetInnerHTML={{ __html: svg }}
/>
);
}Why This Works
startOnLoad: false— Mermaid won't try to scan the DOM automatically; we callmermaid.render()manually.useEffectwith cleanup — thecancelledflag prevents stale renders from updating state after a prop change races against a new render.- Unique IDs —
mermaid.render()creates temporary DOM nodes; unique IDs prevent ID collisions when multiple diagrams are on the same page. dangerouslySetInnerHTML— Mermaid returns a finished SVG string. Setting it viainnerHTMLis the correct approach; the content comes from the Mermaid library, not user input.
Usage
import MermaidDiagram from '@/components/MermaidDiagram';
const chart = `
flowchart LR
A[User] --> B[API]
B --> C[(Database)]
B --> D[Cache]
`;
export default function DocsPage() {
return (
<div>
<h2>System Architecture</h2>
<MermaidDiagram chart={chart} />
</div>
);
}Dynamic Charts with User Input
One powerful use case is letting users write Mermaid syntax and see a live preview — exactly what MermaidEditor.lol does.
'use client';
import { useState } from 'react';
import MermaidDiagram from '@/components/MermaidDiagram';
const defaultChart = `flowchart TD
A[Start] --> B{Decision}
B -->|Yes| C[Action 1]
B -->|No| D[Action 2]`;
export default function LiveEditor() {
const [chart, setChart] = useState(defaultChart);
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<textarea
value={chart}
onChange={(e) => setChart(e.target.value)}
rows={12}
style={{ fontFamily: 'monospace', fontSize: 13 }}
/>
<MermaidDiagram chart={chart} />
</div>
);
}Because MermaidDiagram re-renders whenever the chart prop changes, the preview updates in real time as the user types.
Dark Mode Support
Mermaid ships with a dark theme. Combine it with your app's color scheme preference:
'use client';
import { useEffect, useState } from 'react';
import MermaidDiagram from '@/components/MermaidDiagram';
const chart = `sequenceDiagram
Client->>Server: GET /api/data
Server-->>Client: JSON response`;
export default function ThemedDiagram() {
const [theme, setTheme] = useState<'default' | 'dark'>('default');
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
setTheme(mq.matches ? 'dark' : 'default');
const handler = (e: MediaQueryListEvent) =>
setTheme(e.matches ? 'dark' : 'default');
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return <MermaidDiagram chart={chart} theme={theme} />;
}Next.js App Router and SSR
If you're using Next.js App Router (Next.js 13+), Mermaid cannot run on the server — it needs document and window. The 'use client' directive at the top of MermaidDiagram.tsx handles this, but there's one more step if you import it in a Server Component:
// app/docs/page.tsx (Server Component)
import dynamic from 'next/dynamic';
const MermaidDiagram = dynamic(() => import('@/components/MermaidDiagram'), {
ssr: false,
loading: () => <div style={{ height: 200, background: '#f3f4f6' }} />,
});
const chart = `flowchart LR
A --> B --> C`;
export default function DocsPage() {
return (
<main>
<h1>Architecture</h1>
<MermaidDiagram chart={chart} />
</main>
);
}ssr: false tells Next.js to only load and render this component in the browser. The loading fallback prevents layout shift while the component hydrates.
Rendering Multiple Diagrams
When a page has several diagrams, the component handles them independently — each renders with its own unique ID and lifecycle.
const diagrams = [
{ title: 'User Flow', chart: 'flowchart LR\n A --> B --> C' },
{ title: 'Sequence', chart: 'sequenceDiagram\n Alice->>Bob: Hello\n Bob-->>Alice: Hi!' },
{ title: 'ER Model', chart: 'erDiagram\n USER ||--o{ ORDER : places' },
];
export default function DiagramGallery() {
return (
<div>
{diagrams.map(({ title, chart }) => (
<section key={title}>
<h3>{title}</h3>
<MermaidDiagram chart={chart} />
</section>
))}
</div>
);
}Debouncing for Live Editors
If you're building a live editor (textarea to diagram preview), debounce the chart prop to avoid triggering a Mermaid render on every keystroke:
import { useState, useEffect } from 'react';
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
export default function LiveEditor() {
const [raw, setRaw] = useState('flowchart TD\n A --> B');
const debouncedChart = useDebounce(raw, 300);
return (
<div style={{ display: 'flex', gap: 16 }}>
<textarea value={raw} onChange={(e) => setRaw(e.target.value)} rows={10} />
<MermaidDiagram chart={debouncedChart} />
</div>
);
}A 300ms debounce gives snappy feedback without hammering the renderer on every keypress.
Common Errors and Fixes
document is not defined on SSR
Add 'use client' to the component, or use dynamic(() => import(...), { ssr: false }) in Next.js.
Old SVGs accumulating in the DOM
This means you're using mermaid.contentLoaded() or mermaid.init() instead of mermaid.render(). The component above uses render() which returns the SVG as a string — no DOM accumulation.
Syntax error crashes the whole page
The try/catch in the component catches Mermaid's parse errors and renders an error message instead of throwing.
Diagrams look fine then disappear on re-render
You're likely setting initialized inside the component rather than at module scope. If the component unmounts and remounts, Mermaid initialize() gets called again with startOnLoad: true — it then tries to re-process all .mermaid divs and stomps over React's rendered content.
Quick Reference
// Minimal working component
'use client';
import { useEffect, useState } from 'react';
import mermaid from 'mermaid';
mermaid.initialize({ startOnLoad: false, theme: 'default' });
export function Diagram({ chart }: { chart: string }) {
const [svg, setSvg] = useState('');
useEffect(() => {
mermaid.render('d-' + Date.now(), chart).then(({ svg }) => setSvg(svg));
}, [chart]);
return <div dangerouslySetInnerHTML={{ __html: svg }} />;
}This stripped-down version is fine for quick prototypes. Use the full component above for production.
Conclusion
Integrating Mermaid.js into React comes down to three rules: initialize once at module scope, call mermaid.render() inside useEffect, and use 'use client' (or dynamic with ssr: false) in Next.js. Once the component is in place, rendering any diagram type is just a prop change.