Skip to main content

React Mermaid, no Mermaid.js runtime

@react-markdown-kit/mermaid renders a ```mermaid fence in React Markdown as static SVG. It does not load Mermaid.js. It renders flowcharts only. The same plugin opens the fence on a drawing canvas in the editor and writes plain Mermaid back.

Try the canvas at mermaid.reactmarkdownkit.com. This page is for the React side: install, render, server-render, edit, and the one comment that stores positions.

Install

npm install @react-markdown-kit/renderer @react-markdown-kit/mermaid

The renderer is a peer. The plugin's root entry has no runtime dependency, no React import and no Lexical import, which scripts/pack-check.mjs proves by installing the packed tarball into a consumer without Lexical and rendering a diagram.

Render a flowchart in nine lines

import Markdown, { defineMarkdownPreset } from '@react-markdown-kit/renderer'
import { mermaid } from '@react-markdown-kit/mermaid'
import '@react-markdown-kit/mermaid/styles.css' // optional: surface, border, dot grid

const preset = defineMarkdownPreset({ extensions: [mermaid()] })

export function Doc({ content }: { content: string }) {
return <Markdown preset={preset}>{content}</Markdown>
}

mermaid() is the whole API. It turns a mermaid fence whose body is a flowchart into a diagram node, and the renderer draws that node as SVG. Every other fence, and every Mermaid diagram that is not a flowchart, stays an ordinary code block.

The example below is rendered by the plugin, on this page, with no browser work: the SVG is in the HTML the server sent.

Rendered by @react-markdown-kit/mermaid, editable
Markdown (edit me)
Rendered
DiagramRequestCompiled documentReact elementsStatic HTMLparse

Static SVG, no runtime

The output is one figure holding one svg.

<figure data-rmk-diagram="mermaid">
<svg viewBox="0 0 640 260" role="img" aria-label="Diagram"></svg>
</figure>
  • Shapes are rect, ellipse, circle, polygon, path and text, grouped in g, with a title and desc. Colours from style lines land as attributes.
  • No script, no event attribute, no foreignObject, no external reference. The test "does not execute anything" feeds the renderer a hostile drawing payload and a hostile flowchart and asserts none of them appear.
  • A flowchart the parser cannot read is shown as source inside the figure, with data-rmk-diagram-error set and a DIAGRAM_INVALID diagnostic on the compiled document. Nothing is hidden and nothing throws.

Size, measured

Mermaid.js ships a parser and a layout engine for every diagram type. This plugin parses one type and lays it out itself, so it is smaller. The table is generated by scripts/mermaid-size.mjs and stored in docs/data/mermaid-size.json, measured on 2026-09-20. Gzip is node:zlib gzipSync, level 9, one file at a time. 1 KB is 1024 bytes.

What loadsFilesMinifiedGzipped
@react-markdown-kit/mermaid@0.1.0, root entry152.9 KB18.1 KB
mermaid@12.0.0, entry only129.5 KB11.1 KB
mermaid@12.0.0, entry plus the chunks one flowchart loads28861.3 KB237.1 KB
mermaid@12.0.0, entry plus every chunk1055329.9 KB1572.1 KB

How each row was measured:

  • The plugin row is plugins/mermaid/dist/index.js bundled and minified with esbuild (esbuild 0.27.7: --bundle --minify --format=esm, React and Lexical external), then gzipped.
  • The Mermaid rows are the published package, fetched with npm pack (npm pack mermaid@12.0.0; dist/mermaid.esm.min.mjs and dist/chunks/mermaid.esm.min/*.mjs as published, already minified). Its entry, {size.mermaid.entry.file}, lazy-loads one chunk per diagram type. The flowchart row is the static import closure of the entry, the flowDiagram chunk and the dagre layout chunk it requests. The last row is every chunk in the package. Each row sums the files' sizes, gzipped one file at a time.
  • React is not counted on either side. The renderer package is not counted either; it is the Markdown pipeline the plugin plugs into.

For one flowchart, that is 13 times less gzipped JavaScript. Mermaid.js does far more with its bytes. If the document needs a sequence diagram, this plugin will not draw it.

Server rendering

The SVG is built as hast, with no DOM, so the plugin works wherever the renderer works: a React server component, SSR, a static build, or a Node service that only compiles or templates the document with compileMarkdown and installs no Lexical (scripts/pack-check.mjs).

// app/docs/[slug]/page.tsx, a server component
import { readFile } from 'node:fs/promises'
import Markdown, { defineMarkdownPreset } from '@react-markdown-kit/renderer'
import { mermaid } from '@react-markdown-kit/mermaid'

const preset = defineMarkdownPreset({ extensions: [mermaid()] })

export default async function Page({ params }) {
const content = await readFile(`content/${params.slug}.md`, 'utf8')
return <Markdown preset={preset}>{content}</Markdown>
}

The plugin's own tests render through renderToStaticMarkup, and the diagram on this page was server-rendered by Docusaurus the same way. The Next.js guide covers precompiling a document once per process: Markdown in Next.js.

Edit the diagram on a canvas

@react-markdown-kit/mermaid/editor is the same extension with one more capability. It replaces mermaid() from the root entry in an editor preset, and only this entry loads React and Lexical.

import { MarkdownEditor } from '@react-markdown-kit/editor'
import { defineMarkdownPreset, gfm } from '@react-markdown-kit/renderer'
import { mermaid } from '@react-markdown-kit/mermaid/editor'

const preset = defineMarkdownPreset({ extensions: [gfm(), mermaid()] })

<MarkdownEditor preset={preset} value={value} onChange={setValue} />

The toolbar gains an insert-diagram button. Boxes and connectors are drawn by dragging. Connectors bind to the boxes they touch and follow them. Copy as Mermaid copies the same text the fence saves, layout comment included, so other renderers ignore the last line and this plugin restores the positions.

Loading editor

An untouched fence is written back byte for byte (test). An edited one is written as ```mermaid with the positions on the last line (test).

The layout annotation

Mermaid has no syntax for where a box sits, how big it is, how an edge bends, or free text on the canvas. The editor stores exactly that remainder in one comment on the last line of the fence:

%% rmk-layout v1 {"canvasHeight":180,"nodes":{"a":{"x":24,"y":40,"width":160,"height":90}},"edges":{"a->b":{"routing":"elbow"}}}

%% starts a comment in Mermaid (flowchart syntax, comments), so Mermaid.js and the hosts that render fences with it, such as GitHub, ignore the line and lay the flowchart out their own way (host documentation). This plugin reads it back, so the drawing round-trips without loss (round-trip tests). A hand-written flowchart with no annotation is auto-laid out.

With a layout annotation: exact positions and elbow routing
Markdown
```mermaid
flowchart LR
    a["Markdown"]
    b(["React"])
    a -->|render| b
    style a fill:#a5d8ff,stroke:#1971c2
    style b fill:#b2f2bb,stroke:#2f9e44
    %% rmk-layout v1 {"canvasHeight":180,"nodes":{"a":{"x":24,"y":40,"width":160,"height":90,"strokeWidth":2},"b":{"x":330,"y":40,"width":170,"height":90,"strokeWidth":2}},"edges":{"a->b":{"routing":"elbow","elbow":0.5}}}
```
Rendered
DiagramMarkdownReactrender

The syntax stays authoritative for the graph and the colours. The annotation never repeats them. Edges are keyed by their endpoints (a->b, a->b#2 for a second edge between the same nodes), so an entry survives edits that reorder the statements. Reading is fail-closed: a payload with a wrong type, an unknown version or a second annotation is rejected as a whole, the diagram is auto-laid out, and the compile result carries a DIAGRAM_LAYOUT_INVALID warning with the path of the first problem (validation tests). Members the reader does not know are ignored, so a later minor revision can add fields.

The format is specified in LAYOUT_ANNOTATION.md, shipped with the package, and LAYOUT_ANNOTATION_JSON_SCHEMA is exported from the root.

Flowcharts only

The plugin renders flowcharts only. flowchart or graph, any direction, the bracket shapes ([ ], ( ), ([ ]), (( )), { }, {{ }}, [( )], [[ ]], > ]), edge labels in both spellings, lines, arrows, bidirectional arrows, chains, & groups, subgraph … end read through, style colours and a front-matter title. classDef, click and linkStyle are ignored.

Sequence, class, state, Gantt, pie and every other diagram type stay ordinary code blocks: rendered as source, edited as text, never opened on the canvas (test). The subset is covered by mermaid-parse.test.ts. If a document needs those types, render them with Mermaid.js; the two can share a page, since this plugin leaves the fences it does not read untouched.

FAQ

Does React Mermaid need Mermaid.js?

No. @react-markdown-kit/mermaid parses the flowchart subset itself and draws static SVG, so nothing loads Mermaid.js. The plugin is 18.1 KB gzipped; the files Mermaid.js 12.0.0 loads for one flowchart are 237.1 KB gzipped, measured by scripts/mermaid-size.mjs.

Can I render Mermaid in a React server component?

Yes. The SVG is built as hast with no DOM and no browser global, so <Markdown> with the mermaid() extension renders in a server component, during SSR and in a static build. The plugin tests render it with renderToStaticMarkup.

Which Mermaid diagram types does the plugin render?

Flowcharts only, written as flowchart or graph in any direction. Sequence, class, Gantt, pie and every other Mermaid diagram type stay ordinary code blocks, rendered as source.

Does the layout comment break GitHub or Mermaid.js?

No. Positions are stored in one %% rmk-layout v1 comment, and %% starts a comment in Mermaid (mermaid.js.org/syntax/flowchart.html#comments), so Mermaid.js and the hosts that render fences with it, such as GitHub, ignore the line and draw the flowchart with their own layout.

Next

Mermaid visual editor · @react-markdown-kit/mermaid on npm · Plugin reference · Mermaid live editor alternative · Flowchart to Mermaid · Fix AI-generated Mermaid · Source on GitHub