Gagan Thakur·

How to Use Mermaid.js in Next.js (App Router & Pages Router)

Step-by-step guide to integrating Mermaid.js diagrams in Next.js. Covers dynamic imports, SSR pitfalls, custom hooks, and rendering flowcharts, sequence diagrams, and ER diagrams in your Next.js app.

# How to Use Mermaid.js in Next.js (App Router & Pages Router)

Mermaid.js is one of the most popular libraries for rendering text-based diagrams — flowcharts, sequence diagrams, ER diagrams, class diagrams, and more. If you're building a Next.js app and want to embed live diagrams, you'll hit a few SSR-specific gotchas. This guide walks you through everything, from installation to a production-ready component.

---

Why Mermaid.js + Next.js Is Tricky

Mermaid.js is a browser-only library. It manipulates the DOM directly during rendering. Next.js runs code on the server first (SSR / RSC), which means:

- window and document don't exist on the server

- Importing Mermaid at the module level throws ReferenceError: window is not defined

- You must use dynamic imports inside useEffect or with next/dynamic

Once you understand this, the integration is straightforward.

---

Installation

npm install mermaid

No extra type package is needed — Mermaid ships its own TypeScript definitions.

---

Approach 1: Dynamic Import with next/dynamic (Pages Router)

This is the simplest approach for the Pages Router. Create a wrapper component and load it client-side only.

components/MermaidDiagram.tsx

import { useEffect, useRef } from "react";

interface MermaidDiagramProps {
  chart: string;
}

export default function MermaidDiagram({ chart }: MermaidDiagramProps) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let cancelled = false;

    async function render() {
      const mermaid = (await import("mermaid")).default;

      mermaid.initialize({
        startOnLoad: false,
        theme: "default",
        securityLevel: "loose",
      });

      if (ref.current && !cancelled) {
        ref.current.innerHTML = "";
        const id = "mermaid-" + Math.random().toString(36).slice(2);
        const { svg } = await mermaid.render(id, chart);
        if (ref.current && !cancelled) {
          ref.current.innerHTML = svg;
        }
      }
    }

    render();
    return () => { cancelled = true; };
  }, [chart]);

  return <div ref={ref} className="mermaid-container" />;
}

In your page (Pages Router):

import dynamic from "next/dynamic";

const MermaidDiagram = dynamic(() => import("../components/MermaidDiagram"), {
  ssr: false,
  loading: () => <p>Loading diagram...</p>,
});

export default function DocsPage() {
  const chart = `flowchart TD
    A[User Request] --> B{Auth Check}
    B -- Pass --> C[Load Data]
    B -- Fail --> D[401 Error]
    C --> E[Return Response]`;

  return (
    <main>
      <h1>Architecture Overview</h1>
      <MermaidDiagram chart={chart} />
    </main>
  );
}

---

Approach 2: App Router with "use client" Directive

In the Next.js App Router, server components are the default. Mark your Mermaid component as a client component with "use client" at the top — no next/dynamic wrapper needed.

"use client";

import { useEffect, useRef, useState } from "react";

export default function MermaidDiagram({ chart }: { chart: string }) {
  const ref = useRef<HTMLDivElement>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (!ref.current) return;
    let active = true;

    (async () => {
      try {
        const mermaid = (await import("mermaid")).default;
        mermaid.initialize({ startOnLoad: false, theme: "neutral" });
        const id = "mmd-" + Date.now();
        const { svg } = await mermaid.render(id, chart);
        if (active && ref.current) ref.current.innerHTML = svg;
      } catch (e) {
        if (active) setError(String(e));
      }
    })();

    return () => { active = false; };
  }, [chart]);

  if (error) return <pre style={{ color: "red" }}>{error}</pre>;
  return <div ref={ref} />;
}

Use it in any server component page:

// app/docs/page.tsx (Server Component — fine to import a client child)
import MermaidDiagram from "@/components/MermaidDiagram";

const chart = `sequenceDiagram
    Client->>API: POST /login
    API->>DB: SELECT user
    DB-->>API: user row
    API-->>Client: 200 OK + JWT`;

export default function DocsPage() {
  return (
    <section>
      <h2>Authentication Flow</h2>
      <MermaidDiagram chart={chart} />
    </section>
  );
}

---

Dark Mode Support

Pass the theme option to mermaid.initialize(). To respect the user's OS preference:

const prefersDark =
  typeof window !== "undefined" &&
  window.matchMedia("(prefers-color-scheme: dark)").matches;

mermaid.initialize({
  startOnLoad: false,
  theme: prefersDark ? "dark" : "default",
});

Available built-in themes: default, neutral, dark, forest, base.

---

Rendering Multiple Diagrams

Each call to mermaid.render() needs a unique element ID. Use a timestamp or UUID:

const uniqueId = "mermaid-" + Math.random().toString(36).slice(2, 9);
const { svg } = await mermaid.render(uniqueId, chart);

If you reuse IDs across re-renders, Mermaid throws Element with id already exists.

---

Common Errors and Fixes

ReferenceError: window is not defined

- Cause: Mermaid imported at module level

- Fix: Move import inside useEffect or use dynamic import

Element with id already exists

- Cause: Same ID passed to mermaid.render() twice

- Fix: Generate a unique ID each render cycle

Blank output, no error

- Cause: ref.current is null when render runs

- Fix: Check the effect dependency array — ensure it runs after mount

SVG renders but styles are missing

- Cause: CSP blocking inline styles

- Fix: Add style-src 'unsafe-inline' to your Content-Security-Policy header

---

ER Diagram Example

erDiagram
    USER {
        int id PK
        string email
        string name
    }
    POST {
        int id PK
        string title
        int userId FK
    }
    COMMENT {
        int id PK
        string body
        int postId FK
    }
    USER ||--o{ POST : writes
    POST ||--o{ COMMENT : has

Pass this string directly to to render a live ER diagram in your Next.js database schema docs page.

---

Try It Without Installing Anything

If you just need to prototype a diagram, MermaidEditor.lol lets you write and preview Mermaid diagrams instantly in the browser — no setup required. Copy the output SVG directly into your Next.js project.

---

Summary

- Always import Mermaid dynamically or inside useEffect — never at the module level

- Use "use client" in App Router, or next/dynamic({ ssr: false }) in Pages Router

- Generate unique IDs for each mermaid.render() call

- Use the theme option to match your app's color scheme

- Catch render errors and surface them — malformed syntax throws instead of returning empty SVG

With this setup, you can embed fully rendered flowcharts, sequence diagrams, class diagrams, and any other Mermaid diagram type directly inside your Next.js application.