mrkeyoor.com_
Thu 06 Aug 15:43 UTC
npmWeb Frontendupdated 06 Aug 2026

mermaid

Mermaid turns a few lines of text into an SVG diagram. You write something like 'flowchart LR\n a --> b' and mermaid gives you back SVG markup you drop into the page. It ships grammars for flowcharts, sequence diagrams, class diagrams, entity relationship diagrams, state diagrams, gantt charts, git graphs, pie charts, mindmaps, timelines, quadrant charts, sankey diagrams, architecture diagrams and a few more. The whole point is that a diagram lives in your repository as text, so it diffs in a pull request and nobody has to open Visio to move one box. GitHub, GitLab, Notion and Obsidian all render mermaid blocks in markdown, which is why most people meet it there first rather than as an npm dependency. As a library it is a browser thing: mermaid.render() needs a DOM to measure text before it can lay anything out.

Verdict

For diagrams-as-text in docs and repos, mermaid is the default and deserves to be. Just do not put it in a latency-sensitive bundle or expect it to render untrusted input safely without securityLevel set to sandbox.

API stability4/5The runtime surface has been small and steady since v10 made render() async and returns a promise: initialize, run, render, parse. Diagram syntax keeps growing and rarely breaks. The friction is packaging, not the API: v10 dropped CommonJS and v11 reorganised the default entry, so upgrades tend to break your build config rather than your calls.
Docs4/5mermaid.js.org documents every diagram type with live editable examples, and the config reference lists each option with defaults. The integration side is weaker: the React, Next.js and server-rendering stories are mostly community blog posts, and the theming docs stop short of explaining which CSS variables actually survive into the SVG.
Maintenance5/511.16.1 published 4 August 2026 with the repo pushed 6 August 2026, releases every few weeks, and a named maintainer team plus a commercial sponsor behind it. The 1675 open items on GitHub read alarming until you split them: 1428 are issues and the rest are PRs, which is normal backlog for a project this widely embedded.
Ecosystem5/5Rendered natively by GitHub, GitLab, Azure DevOps, Notion and Obsidian, so the syntax is portable far beyond your app. First-party add-ons cover ELK layout and Iconify icon packs, and mermaid-cli covers headless rendering.

Use it if

  • Your diagrams should live in git next to the code and be reviewable as a text diff instead of being a binary file someone re-exports every few months
  • You are building docs, a wiki, or an internal tool and want authors to write diagrams in a markdown code fence without learning a drawing tool
  • You need several diagram families (sequence, ER, state, gantt, flowchart) from one dependency and one syntax family rather than four different renderers
  • You want the output as inline SVG you can style with CSS variables and your own theme, not a raster image or an iframe to someone else's service
Skip it if

Setup reality

npm install mermaid gets you an ESM-only package: the exports map points at dist/mermaid.core.mjs and there is no CommonJS build, so require('mermaid') fails and older Jest setups need transformIgnorePatterns adjusted or a jsdom environment plus ESM support. The default entry lazy loads each diagram type as a separate chunk, which is what you want in a bundler and what breaks if your build tool cannot handle dynamic import (Vite is fine, older webpack configs and some Next.js server components are not). In React you almost always end up with a client-only component: call mermaid.initialize({ startOnLoad: false }) once at module scope, then await mermaid.render() inside an effect, because rendering twice under StrictMode with the same element id throws. Rendering also needs the element to be in the document and visible enough to measure, so a diagram inside a display:none tab renders at zero width. Fonts must be loaded before render or every label is measured against a fallback face and boxes come out the wrong size.

Patterns

Render one diagram and insert the SVGrender-to-svg

import mermaid from 'mermaid';

mermaid.initialize({ startOnLoad: false, theme: 'default' });

const el = document.querySelector('#graph');
const { svg, bindFunctions } = await mermaid.render(
  'graph-1',
  'flowchart LR\n  a[Start] --> b{Ok?}\n  b -->|yes| c[Done]\n  b -->|no| a',
);
el.innerHTML = svg;
bindFunctions?.(el);

The first argument is the id given to the generated SVG element, and it must be unique on the page. Calling render twice with the same id leaves a stale temporary div in the body. bindFunctions attaches click handlers and tooltips; skip it and interactive diagrams are inert.

Render every .mermaid block already in the DOMauto-render-page

import mermaid from 'mermaid';

mermaid.initialize({ startOnLoad: false });
await mermaid.run({ querySelector: '.mermaid' });

// or re-run over a specific set after new content arrives
await mermaid.run({ nodes: document.querySelectorAll('.mermaid:not([data-processed])') });

run() marks each element with data-processed, so calling it again is cheap but will not re-render edited source unless you clear that attribute and restore the original text. Prefer this over startOnLoad: true, which fires on DOMContentLoaded and races with anything that injects markdown later.

Check syntax without throwingvalidate-before-render

const result = await mermaid.parse(source, { suppressErrors: true });
if (result === false) {
  showEditorError('That is not valid mermaid');
} else {
  console.log(result.diagramType); // e.g. 'flowchart-v2'
}

Without suppressErrors, parse() throws and render() will paint a red error diagram into your container. Set suppressErrorRendering: true in initialize if you would rather handle failures yourself than have mermaid write an error graphic into the page.

A React component that survives StrictModereact-client-component

'use client';
import { useEffect, useId, useRef, useState } from 'react';
import mermaid from 'mermaid';

mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' });

export function Diagram({ chart }: { chart: string }) {
  const id = useId().replace(/:/g, '');
  const ref = useRef<HTMLDivElement>(null);
  const [svg, setSvg] = useState('');

  useEffect(() => {
    let cancelled = false;
    mermaid
      .render(`m-${id}`, chart)
      .then(({ svg }) => { if (!cancelled) setSvg(svg); })
      .catch(() => { if (!cancelled) setSvg(''); });
    return () => { cancelled = true; };
  }, [chart, id]);

  return <div ref={ref} dangerouslySetInnerHTML={{ __html: svg }} />;
}

useId gives a stable unique id per instance; the colons it contains are not valid in a CSS selector so strip them. The cancelled flag matters because StrictMode runs the effect twice in development and the two renders resolve out of order. This must be a client component: mermaid touches document at import time.

Set a theme and override colours globallytheme-and-config

mermaid.initialize({
  startOnLoad: false,
  theme: 'base',
  themeVariables: {
    primaryColor: '#101418',
    primaryTextColor: '#e6e6e6',
    primaryBorderColor: '#2a3038',
    lineColor: '#5b6472',
    fontFamily: 'ui-sans-serif, system-ui, sans-serif',
  },
  flowchart: { curve: 'basis', htmlLabels: true },
});

themeVariables only has an effect with theme: 'base'; pick 'dark' or 'forest' and your overrides are mostly ignored. initialize merges into the current config rather than replacing it, so calling it twice with different objects leaves you with the union of both.

Override config for a single diagramper-diagram-directive

const chart = `%%{init: {'theme':'base','themeVariables':{'lineColor':'#ff6b6b'}}}%%
sequenceDiagram
  Client->>Server: subscribe
  Server-->>Client: ack`;

const { svg } = await mermaid.render('seq-1', chart);

The init directive must be the first line of the source. It is real config injection, which is exactly why you should not run it over diagram text submitted by users: the directive can change securityLevel-adjacent rendering options that you set globally.

Wire node clicks to your own codeclickable-nodes

mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' });

window.openService = (id) => router.push(`/services/${id}`);

const chart = `flowchart TD
  api[API] --> db[(Postgres)]
  click api call openService("api")
  click db href "/services/db" "Open"`;

const el = document.querySelector('#graph');
const { svg, bindFunctions } = await mermaid.render('svc', chart);
el.innerHTML = svg;
bindFunctions?.(el);

click ... call needs securityLevel 'loose', which also means any diagram source rendered by that mermaid instance can invoke globals. Keep loose for diagrams you author and strict for anything else, and remember the callback has to be reachable from window.

Keep a pathological diagram from freezing the tabguard-large-diagrams

mermaid.initialize({
  startOnLoad: false,
  maxTextSize: 50_000, // default, characters of source
  maxEdges: 500,       // default, edges per diagram
});

if (source.length > 50_000) {
  throw new Error('Diagram source too large to render inline');
}

Layout is synchronous once it starts, so a 2000-edge flowchart locks the main thread rather than failing fast. Raising maxEdges is usually the wrong fix: split the diagram, or render it to a static SVG at build time.

Swap dagre for ELK on dense flowchartselk-layout

// npm i @mermaid-js/layout-elk
import mermaid from 'mermaid';
import elkLayouts from '@mermaid-js/layout-elk';

mermaid.registerLayoutLoaders(elkLayouts);
mermaid.initialize({ startOnLoad: false, layout: 'elk' });

// or per diagram:
// %%{init: {'layout': 'elk'}}%%

ELK produces far fewer edge crossings on wide graphs and is noticeably slower. It only applies to flowcharts and state diagrams; sequence and gantt ignore the layout setting entirely.

Load Iconify packs for architecture diagramsregister-icon-packs

import mermaid from 'mermaid';

mermaid.registerIconPacks([
  {
    name: 'logos',
    loader: () => fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then((r) => r.json()),
  },
]);

const chart = `architecture-beta
  group api(logos:aws-lambda)[API]
  service db(logos:postgresql)[Database] in api`;

The loader is async and mermaid awaits it during render, so a slow or blocked CDN stalls the diagram. Icons that fail to resolve render as a placeholder question mark rather than throwing, which makes typos in icon names easy to miss.

Make rendered SVG stable across runsdeterministic-ids-for-tests

mermaid.initialize({
  startOnLoad: false,
  deterministicIds: true,
  deterministicIDSeed: 'snapshot',
});

const { svg } = await mermaid.render('fixture', chart);
expect(svg).toMatchSnapshot();

Without this, mermaid generates random internal ids and every snapshot diff is noise. It does not make output stable across mermaid versions or across machines with different fonts, so pin the version and expect snapshots to churn on upgrade.

Produce SVG outside a browserrender-in-node

# the supported path: headless Chromium via puppeteer
npx -p @mermaid-js/mermaid-cli mmdc -i diagram.mmd -o diagram.svg

# in CI, point it at a config file to control theme and background
npx -p @mermaid-js/mermaid-cli mmdc \
  -i docs/arch.mmd -o docs/arch.svg \
  -b transparent -t dark

Importing mermaid into plain Node and handing it a jsdom document mostly works until text measurement, which jsdom does not implement, so every label collapses to zero width and boxes overlap. mermaid-cli launches real Chromium, which is the reason it works and also the reason your CI image grows by a few hundred megabytes.

Alternatives

PackageRegistryPick it when
@mermaid-js/mermaid-clinpmYou only need static SVG or PNG files generated in CI and would rather not ship the renderer to the browser at all
@viz-js/viznpmYou want Graphviz DOT layout quality for pure node-and-edge graphs, compiled to WebAssembly with no DOM measurement step
nomnomlnpmYou only draw UML-style class and component diagrams and want a much smaller dependency