Skip to content

Mermaid diagrams

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


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


<Markdown preset={preset}>{content}</Markdown>

You can try it in the Mermaid live editor. The syntax is on the left and the canvas (or the preview) is on the right, and what it saves is plain Mermaid.

A diagram is a fenced code block whose language is mermaid. The document stays plain Markdown with plain Mermaid in it, so GitHub, GitLab, Notion and Obsidian render the same fence. mermaid() is the package's API, and it turns every ```mermaid fence into a diagram node. The renderer draws flowcharts and sequence diagrams as static SVG with no Mermaid.js at runtime, shows every other Mermaid type as source, and lets a host render those with its own Mermaid.js through the fallback entry. The variables plugin leaves the node literal. In the editor, flowcharts and sequence diagrams each get their own canvas, and every other kind opens in a source editor with a live preview.

A three-box flow
Markdown
```mermaid
flowchart LR
    web["CLIENT<br/>Web App<br/>React"] -->|REST| api["SERVICE<br/>API<br/>Kotlin"]
    api --> db[(Postgres)]
    style web fill:#a5d8ff,stroke:#1971c2
    style api fill:#b2f2bb,stroke:#2f9e44
```
Rendered
DiagramCLIENTWeb AppReactSERVICEAPIKotlinPostgresREST

Guides#

This page is the reference. If you're after one specific job, one of these guides is probably quicker:

Diagram kinds#

The plugin reads the first word of the fence, after front matter, %%{ … }%% directives and %% comments, the way Mermaid's own detectType does. The word names the kind. If the plugin has that kind registered it renders it. Any other Mermaid keyword is still recognised, it just gets shown as source.

KindHeaderRenderedEdited
Flowchartflowchart, graph, flowchart-elkStatic SVGCanvas, or text
Sequence diagramsequenceDiagramStatic SVGCanvas, or text
Every other Mermaid type (classDiagram, stateDiagram, erDiagram, gantt, pie, gitGraph, mindmap, …)its keywordSource, or the host fallbackText

Each fence carries a support level: static when a registered kind parses and renders it, source otherwise. A fence that starts with no Mermaid keyword is kind unknown, support source. Keywords are case-sensitive, as in Mermaid; SequenceDiagram is unknown, and the diagnostic names the expected spelling.

Kinds are configuration values, like gfm(). The default registry is [flowchart(), sequenceDiagram()], tried in that order. You can pass your own list to trim the bundle or add a kind of your own:

import { mermaid, flowchart } from '@react-markdown-kit/mermaid'


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

Two kinds that share a name or a keyword make mermaid() throw MarkdownConfigurationError with code DIAGRAM_KINDS_INVALID.

The flowchart subset#

MermaidRead as
flowchart LR / graph TD, any directionLeft-to-right or top-down auto layout
id[text], id(text)Rectangle
id([text]), id((text))Ellipse
id{text}, id{{text}}Diamond
id[(text)]Cylinder
id[[text]]Queue
id>text]Note
-->, ---, -.->, ==>, <-->Arrow, line, arrow, arrow, bidirectional arrow
-->|label|, -- label -->Edge label
A-->B, A --> BThe same edge, with or without spaces
A & B --> C, A --> B --> C, ;Groups, chains, several statements per line
subgraph … endRead through; grouping is not drawn
style id fill:…,stroke:…Colours
--- front matter with title:Accessible name and caption
%% comments, %%{ … }%% directives, classDef, class, clickRetained lines: kept in the fence, written back unchanged
@{ … } node config, e1@ edge ids, ~~~ invisible linksRetained lines, reported as FLOWCHART_SYNTAX_IGNORED
Anything elseRetained line, reported as FLOWCHART_UNKNOWN_STATEMENT

The flowchart parser reports anything Mermaid.js would reject, so you find out about the mistake before the fence ends up in GitHub or Azure DevOps.

CodeSeverityWhen
FLOWCHART_UNKNOWN_STATEMENTinvalidA statement the parser cannot read, including an end outside a subgraph. Retained.
FLOWCHART_LABEL_NEEDS_QUOTESinvalidAn unquoted label holding (, ), [, ], {, }, " or |. The node stays; the writer quotes it.
FLOWCHART_LABEL_EMPTYinvalidA[], A[""], A() or ||. The node stays with no text.
FLOWCHART_RESERVED_IDinvalidend, subgraph, graph, flowchart, style, classDef, class, click or linkStyle used as a node id. Retained.
FLOWCHART_TRAILING_COMMENTinvalidA %% comment after a statement on the same line. The statement is read; the comment moves to its own line.
FLOWCHART_LAYOUT_INVALIDinvalidA rejected %% rmk-layout annotation, with the dotted path of the first bad member.
FLOWCHART_SYNTAX_IGNOREDignored@{ … } config, e1@ edge ids, ~~~ links, a direction after the first node, a style naming a node that never appears. Retained.
Direction down, a decision
Markdown
```mermaid
flowchart TD
    user((User)) --> check{Valid?}
    check -->|yes| save[Save]
    check -->|no| err>Show error]
    style save fill:#b2f2bb,stroke:#2f9e44
    style err fill:#ffc9c9,stroke:#e03131
```
Rendered
DiagramUserValid?SaveShow erroryesno

The parser is fairly forgiving. A statement it can't model becomes a problem and a retained line, and the rest of the fence carries on, so the diagram still draws and the line still gets written back. The only real parse error is a fence whose first statement isn't a flowchart header; that fence renders as source with DIAGRAM_INVALID. A rejected %% rmk-layout annotation shows up as problem FLOWCHART_LAYOUT_INVALID with the dotted path of the first bad member, and the diagram falls back to auto layout.

The sequence diagram subset#

A sign-in with alt/else, activation and a note
Markdown
```mermaid
sequenceDiagram
    autonumber
    participant B as Browser
    participant A as API
    B->>+A: POST /session
    alt valid credentials
        A-->>B: 200 token
    else invalid
        A-->>-B: 401
    end
    Note over B,A: The token lives in a cookie
```
Rendered
Diagramalt[valid credentials][invalid]BrowserBrowserAPIAPIThe token lives in a cookiePOST /session1200 token24013

Statements are case-insensitive on the keyword and end at a newline or a ;. A line is split at ; before it is read, as Mermaid does, so write #59; for a semicolon in text.

ConstructSourceNotes
Participantparticipant A, participant A as Label, actor A, actor A as Labeldeclaration order; an undeclared id is appended on first use
Boxbox [colour] [label] … endthe colour stays in the label; a box holds participant statements only
MessageA->B: text with ->, -->, ->>, -->>, <<->>, <<-->>, -x, --x, -), --)+ or - after the arrow activates or deactivates
Activationactivate A, deactivate Aan activation still open at the end is a problem
NoteNote left of A: t, Note right of A: t, Note over A: t, Note over A,B: t
Frameloop t, alt t / else t, opt t, par t / and t, critical t / option t, break t, rect colour … endnested arbitrarily; a rect is drawn as a filled rectangle
Autonumberautonumber, autonumber N, autonumber N Mbefore the first message; later forms and autonumber off are retained and ignored
Titlefront matter title:, title X, title: Xcaption and accessible name
Text<br/>, #NN;, #quot;, #lt;, #gt;decoded

Layout is deterministic, so there's no annotation to worry about. Participants are columns in declaration order, each message, note and frame edge takes one row, a self message draws a loop, activations are bars on the lifeline, and frames span the participants they touch. Since the kind has a writer, the editor opens it on the sequence canvas and writes every gesture back as plain Mermaid (see what the sequence writer emits below).

The parser reports what it can't model. invalid problems are statements Mermaid.js itself rejects, and ignored problems are statements Mermaid.js accepts but the plugin doesn't draw. Every ignored statement is kept as a retained line.

CodeSeverityWhen
SEQUENCE_DIAGRAM_UNKNOWN_STATEMENTinvalidA statement that is not sequence diagram syntax. After a ; the message names the entity codes to use instead of ;, &, < and >.
SEQUENCE_DIAGRAM_DEACTIVATE_INACTIVEinvaliddeactivate or a - arrow on a participant that is not active.
SEQUENCE_DIAGRAM_END_WITHOUT_OPENERinvalidAn end with no open frame or box.
SEQUENCE_DIAGRAM_SECTION_OUTSIDE_FRAMEinvalidelse, and or option outside alt, par or critical.
SEQUENCE_DIAGRAM_FRAME_UNCLOSEDinvalidA frame or box still open at the end.
SEQUENCE_DIAGRAM_RESERVED_IDinvalidA message end or note target named like a sequence keyword (end, loop, note, …), which Mermaid.js reads as the keyword.
SEQUENCE_DIAGRAM_ACTIVATION_UNCLOSEDignoredAn activation still open at the end. Mermaid.js accepts it; the bar is drawn to the last item.
SEQUENCE_DIAGRAM_STATEMENT_IGNOREDignoredlink, links, properties, details, create, destroy, accTitle, accDescr, half arrows, () connections, @{ … } participant config, autonumber off, a later autonumber.

Problems point at a line number, a keyword or an id, and don't quote your content. They reach the compiled document as diagnostics (listed under Diagnostics), and the source editor shows them under the textarea with invalid first.

What the editor writes#

Mermaid doesn't have syntax for where a box sits, how big it is, how an edge bends, or free text on the canvas. So when you edit a flowchart, the canvas writes the graph as ordinary Mermaid and puts the geometry in one layout annotation, a %% rmk-layout v1 {…} comment on the last line. Other renderers ignore the comment and this plugin reads it back, which is how a drawing round-trips without loss. A hand-written flowchart with no annotation gets auto layout, and a fence you haven't touched is written back byte for byte.

The writer emits, in order: one --- block holding title: and the retained front-matter lines, the header, node lines, edge lines, style lines, the retained body lines in their original order, and the annotation last. Ids the parser accepted are written unchanged, my-box included; only an id that is not a valid Mermaid id is rewritten. Whatever the property bar sets is written as Mermaid: a rounded rectangle as (text) and a square one as [text], a node's stroke width, dashes and text colour as stroke-width, stroke-dasharray and color on its style line, and an edge's style in its link token (-.-> dotted, ==> thick, --o and --x heads, o--o both ways). A canvas edit can lose things the canvas can't draw, like subgraph, invisible (~~~) and extra-long links, a link whose two heads differ, some bracket shapes, linkStyle and style properties other than the ones above. The parse lists these under lossy, and the editor shows them to you before the first edit.

With a layout annotation: elbow routing and exact positions
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 annotation is a versioned format with a formal specification, LAYOUT_ANNOTATION.md, shipped with the package, and a JSON Schema, LAYOUT_ANNOTATION_JSON_SCHEMA, exported from the root. The payload is one JSON object: nodes keyed by Mermaid id with position, size, stroke width and text slots; edges keyed by endpoints (a->b, a->b#2 for a second edge between the same nodes) with routing, waypoints and attach points; texts and loose for shapes Mermaid can't express. The Mermaid syntax is still the source of truth for the graph and colours, and the annotation doesn't repeat them.

Reading is fail-closed. An annotation with a wrong type, an unknown version or a second annotation in the same fence is rejected as a whole, the diagram is auto-laid out, and the compile result carries a DIAGRAM_LAYOUT_INVALID warning with the dotted path of the first problem (nodes.web.x). Members the reader does not know are ignored, so a later minor revision can add fields without breaking older readers. The writer emits a canonical form (fixed member order, compact JSON, numbers to two decimals), so a diff shows only what moved. readLayoutAnnotation and writeLayoutAnnotation are the reference reader and writer.

What the sequence canvas writes#

A sequence diagram doesn't need an annotation since its layout is deterministic, so the writer emits plain Mermaid and nothing else. In order, that's one --- block holding title: and the retained front-matter lines (omitted when both are empty), the header sequenceDiagram, autonumber when numbering is set, the participants in declaration order as participant id or actor id with as Label when the label differs from the id (each box … end opens where its first participant sits), the retained body lines, then the items depth first at four spaces per level: a message as A->>+B: text, a note as Note over A,B: text, a frame as alt label … else label … end.

Activation bars come from the model's activations. A bar whose messages carry + and - is written with those suffixes; any other bar is written as activate A and deactivate A statements after the items where it starts and ends. Text goes through the same escape as the flowchart writer plus #59; for ;, since sequence text is unquoted: #35; for #, #38;, #37;, #quot;, #lt;, #gt; and <br/> for a line break.

Retained lines are re-emitted verbatim: link, links, properties, details, accTitle, accDescr, %%{ … }%% directives and %% comments. create and destroy can't keep their position among the messages, so the parser lists create-destroy under lossy and the writer drops them. In that case the canvas mounts read-only behind a notice until you choose "Edit on canvas". An untouched fence is written back byte for byte. The written output is a fixed point (it parses back to the same model), and Mermaid.js accepts it for every documentation example in the corpus (conformance.dom.test.ts).

What the renderer emits#

<figure data-rmk-diagram="mermaid" data-rmk-diagram-kind="sequenceDiagram" data-rmk-diagram-support="static">
  <svg viewBox="0 0 640 260" role="img" aria-label="Diagram">…</svg>
  <figcaption>title</figcaption>
</figure>

data-rmk-diagram carries the fence format (mermaid, or the legacy diagram and drawing), data-rmk-diagram-kind the detected kind and data-rmk-diagram-support the support level. A fence shown as source keeps its body inside the figure:

<figure data-rmk-diagram="mermaid" data-rmk-diagram-kind="classDiagram" data-rmk-diagram-support="source">
  <pre><code class="language-mermaid">…</code></pre>
</figure>
  • No script, foreignObject or external references. Shapes are rect, ellipse, path and text. Colours end up as attributes, and the parser drops any value that isn't a colour.
  • Server-renderable. The SVG is hast, built without a DOM or React, so it works in a server component and in static rendering.
  • Only the optional stylesheet styles it. data-rmk-diagram is the hook, and the classNames prop's diagram part adds your own class to the figure.
  • Unreadable input stays visible. A fence a registered kind can't read renders as source inside the figure with data-rmk-diagram-error set and a DIAGRAM_INVALID diagnostic on the compiled document.

Host fallback#

The plugin doesn't depend on Mermaid.js at all. If you want class diagrams, Gantt charts or any other type drawn, the host can render them itself in the browser with @react-markdown-kit/mermaid/client:

import Markdown, { defineMarkdownPreset } from '@react-markdown-kit/renderer'
import { mermaid } from '@react-markdown-kit/mermaid'
import { diagramFallback } from '@react-markdown-kit/mermaid/client'


// Host code: how the SVG is made is the host's choice. Mermaid.js here, but
// Kroki or a server endpoint fit the same signature.
async function render(source: string, kind: string): Promise<string> {
  const { default: mermaidJs } = await import('mermaid')
  const { svg } = await mermaidJs.render(`rmk-${kind}-${Date.now()}`, source)
  return svg
}


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

diagramFallback({ render }) is an extension named mermaid-fallback that registers one renderer component, the figure. It passes every figure through unchanged unless the figure carries data-rmk-diagram-support="source" or data-rmk-diagram-error. For those, the first client render is the server markup, so hydration agrees. After mount it calls render with the fence body and the kind, and a resolved string replaces the pre with <div data-rmk-diagram-fallback> holding the markup. The host picked the renderer, so the host owns that output, and the kit's policy doesn't run on it. A rejected or empty result keeps the source. The entry loads React and is built with the 'use client' banner, so it has to go in a client component (the root entry stays headless).

Theme tokens#

The flowchart takes its colours from the fence. Sequence diagram syntax has no colours, so its renderer reads custom properties instead, each with a fallback from Mermaid's default theme. That way an unstyled document looks like Mermaid, and an exported SVG can inline the fallbacks. The attribute reads fill="var(--rmk-diagram-note-fill, #fff5ad)". styles.css doesn't declare a colour for these tokens, so set them on .rmk-document for each colour mode.

TokenMermaid variableFallback
--rmk-diagram-actor-fillactorBkg#ececff
--rmk-diagram-actor-strokeactorBorder#9370db
--rmk-diagram-actor-textactorTextColor#333333
--rmk-diagram-lineactorLineColor#9370db
--rmk-diagram-signalsignalColor#333333
--rmk-diagram-signal-textsignalTextColor#333333
--rmk-diagram-label-filllabelBoxBkgColor#ececff
--rmk-diagram-label-strokelabelBoxBorderColor#9370db
--rmk-diagram-label-textlabelTextColor#333333
--rmk-diagram-note-fillnoteBkgColor#fff5ad
--rmk-diagram-note-strokenoteBorderColor#aaaa33
--rmk-diagram-note-textnoteTextColor#333333
--rmk-diagram-activation-fillactivationBkgColor#f4f4f4
--rmk-diagram-activation-strokeactivationBorderColor#666666
--rmk-diagram-number-textsequenceNumberColor#ffffff

The autonumber badge is filled with --rmk-diagram-signal. Tokens are named after their role rather than the diagram kind, so a kind added later can reuse them. Front-matter config.theme and themeVariables are kept as retained lines but aren't read.

.rmk-document {
  --rmk-diagram-actor-fill: var(--card);
  --rmk-diagram-actor-stroke: var(--primary);
  --rmk-diagram-note-fill: var(--input-unsaved);
}

Diagnostics#

compileMarkdown reports diagram problems on the document, with the range narrowed to the fence line the problem names. If you lint documents before saving, these are the codes to look for:

CodeSeverityWhen
DIAGRAM_INVALIDwarningA registered kind could not read the fence. It is shown as source.
DIAGRAM_KIND_UNKNOWNwarningThe fence does not start with a Mermaid keyword. A case slip names the expected spelling.
DIAGRAM_KIND_UNSUPPORTEDinfoA Mermaid keyword no registered kind renders. The fence is shown as source.
DIAGRAM_LAYOUT_INVALIDwarningThe %% rmk-layout annotation was rejected; path holds the first bad member.
DIAGRAM_SYNTAX_INVALIDwarningOne per invalid problem, at most 20 per fence: "Line N: …". Mermaid.js would reject the fence.
DIAGRAM_SYNTAX_IGNOREDinfoOne per ignored problem, at most 20 per fence: "Line N: …". Mermaid.js accepts it; the plugin does not draw it.

A parse error's error string lives on the node and isn't copied into the diagnostic message, which keeps your content out of messages.

Variables#

Placeholders inside a diagram are never resolved. The node is literal, same as a code block, so runtime data can't reach a box's text, colour or geometry.

Editor#

@react-markdown-kit/mermaid/editor adds editing. It's the same extension under the same name with one more capability, so it replaces the renderer's mermaid() in a preset.

import { MarkdownEditor } from '@react-markdown-kit/editor'
import { mermaid } from '@react-markdown-kit/mermaid/editor'


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


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

A block shows its kind and support level in a header row. Flowcharts and sequence diagrams open on a canvas, with a Text toggle to switch to the source editor. Every other kind opens in the source editor, which is a textarea with a live preview above it (rendered by the kind) and the problems listed under it with line numbers, invalid first with the prefix "Mermaid rejects:". Source mode doesn't block keystrokes or rewrite your text. Undo goes through the editor's history.

The flowchart canvas#

You draw boxes, connectors and text by picking a tool and dragging. Connectors bind to the boxes they touch and follow them around. The property bar works like Excalidraw's. A colour swatch sets the fill, stroke and text colour in one click, and two custom pickers let you set any background or text colour. Stroke width (thin, medium, bold) and style (solid, dashed, dotted) apply to boxes and connectors, a rectangle can have sharp or round corners, and a connector can have an arrow, circle, cross or no head, elbow routing, and one or two directions. Cmd/Ctrl+D or Duplicate copies the selection. New rectangles are square (Mermaid's [text]). Copy as Mermaid copies the same text the fence saves, layout annotation included, so pasting it into another canvas keeps the positions while other renderers just ignore the comment. A flowchart with lossy features mounts read-only behind a notice that names them, and you pick "Edit on canvas" or "Edit as text" to carry on.

To select several shapes, Shift+click them, drag a box around them on the background, or press Cmd/Ctrl+A. Selection works the way it does in Excalidraw, where every member gets a thin outline and the selection as a whole gets a dashed frame with handles. A corner handle scales the whole selection evenly from the opposite corner, a side handle stretches it along one axis, and Alt scales from the centre; positions, sizes and waypoints scale, text and stroke widths keep their size, and bound connectors follow their boxes. Dragging any member, or the empty space inside the frame, moves the selection; clicking one member without dragging narrows the selection to it; the arrow keys nudge it by 1px, 10px with Shift, as one undo step. The colour and connector settings apply to every selected shape, and a click that moves nothing writes nothing.

The sequence canvas#

You edit a sequence diagram directly on its rendered picture. Each gesture is one operation on the model, written back with the sequence writer and committed as one history entry. Inline typing merges into one entry under the same 300 ms rule as the source editor. Gestures that would make Mermaid.js reject the fence aren't offered. The lossy lock and the "Edit on canvas" notice work the same as for a flowchart.

GestureResult
Click, EscapeSelect one participant, message, note or frame; clear the selection
Delete, BackspaceRemove the selection. A participant takes its messages and notes; a frame unwraps its items
Enter, double clickEdit the label inline. Enter or blur closes the field; Escape restores the text
Add participant, Add actorAppend one with a fresh id and label, selected
Drag a participant box sidewaysReorder the columns; a box moves as one unit
Drag from a lifeline to a lifelineAdd a ->> message at that row, a self message on the same lifeline; its text opens for editing
Drag a message or note up or downReorder it among its siblings
Property bar, participantParticipant or actor
Property bar, messageSolid or dotted line; arrow, open, cross or no head; two-way; activation +, - or none; swap ends
Hover a gap on a lifeline, click +Add a note over that participant at that row
Property bar, noteleft of, right of or over, with a second participant for over
Wrap in, with a message or note selectedWrap it, or a Shift+click range of siblings, in loop, alt, opt, par, critical, break or rect; the label opens for editing
Property bar, frameAdd a section (else, and, option) or unwrap the frame
Drag a frame's bottom edgeExtend or shrink it over the following siblings
AutonumberSet or clear autonumber
Copy as MermaidCopy the text a commit would save

Renaming a participant keeps its id, unless the label was the id, in which case the id follows a sanitised form of the new label. This way hand-written diagrams keep their ids. Read-only editors only show the picture. Every control's label comes from the diagram. label map.

Bringing your own canvas#

mermaid({ editors }) maps a kind name to the component that edits it. The built-in map is { flowchart, sequenceDiagram }. A name in your map replaces the built-in component, and that's also how a kind of your own gets a canvas. The block handles the mode, the lossy lock, undo history and the commit, and the component only has to draw the parse and hand source back.

interface DiagramKindEditorProps {
  readonly nodeKey: NodeKey
  readonly kind: DiagramKind
  readonly source: string
  readonly parse: DiagramParse<unknown>
  /** True while the block locks the canvas behind the lossy notice (a lossy source not yet acknowledged). A read-only editor never mounts the component: the block renders the kind's output instead. */
  readonly readOnly: boolean
  /** Writes `source` to the node as one history entry. `merge` folds it into the previous commit under the 300 ms rule. */
  readonly commit: (source: string, options?: { readonly merge?: boolean }) => void
}
type DiagramKindEditor = ComponentType<DiagramKindEditorProps>
import { mermaid, type DiagramKindEditorProps } from '@react-markdown-kit/mermaid/editor'


function TimelineCanvas({ kind, parse, readOnly, commit }: DiagramKindEditorProps) {
  const model = parse.model as TimelineModel
  const save = (next: TimelineModel) => commit(kind.write!(next, { retained: parse.retained }))
  // The root carries data-rmk-diagram-focus so the block can focus it after an insert.
  return <div className="rmk-diagram-canvas" tabIndex={0} data-rmk-diagram-focus>…</div>
}


const preset = defineMarkdownPreset({
  extensions: [gfm(), mermaid({ kinds: [flowchart(), sequenceDiagram(), timeline()], editors: { timeline: TimelineCanvas } })],
})

A kind only edits on its component if it also has write. Without a writer the block stays in the source editor.

The toolbar gets one insert button per registered kind: id diagram for the flowchart and diagram-<kind> for the others (diagram-sequenceDiagram), group diagram, icon from the kind. You can override a label through the editor's labels prop by that id. The diagram. prefix is reserved for the canvas labels (DIAGRAM_LABELS).

A detached tool row#

Each canvas draws its tool row (the shape tools, the property bar, Copy as Mermaid, the width presets; the participant tools and autonumber on the sequence canvas) along its own top edge. If you build your own chrome around the editor (the way headless editing lets you build your own formatting toolbar), you can move that row out of the block. Mount <DiagramToolbar> from the editor entry wherever you want the tools, inside <MarkdownEditorProvider> or with editor={useMarkdownEditor(...)}, and every canvas of that editor renders its tools there instead.

import { MarkdownEditorContent, MarkdownEditorProvider, useMarkdownEditor } from '@react-markdown-kit/editor'
import { DiagramToolbar, mermaid } from '@react-markdown-kit/mermaid/editor'


function Composer({ value, onChange }) {
  const editor = useMarkdownEditor({ value, onChange, extensions: [mermaid()] })
  return (
    <MarkdownEditorProvider editor={editor}>
      <header className="app-bar">
        <DiagramToolbar placeholder="Open a flowchart or a sequence diagram to draw" />
      </header>
      <div className="rmk-editor">
        <MarkdownEditorContent aria-label="Document" />
      </div>
    </MarkdownEditorProvider>
  )
}

The slot shows one canvas's tools at a time. That's the canvas that last had focus, or the first one on the page if none has had focus yet. If that block is deleted or switched to text, the oldest remaining canvas takes over. The other canvases don't show a row of their own. Focus in the detached row keeps its canvas active, so the property bar and keyboard shortcuts behave the same as they do inline. placeholder renders while no canvas is on screen (the document has none, or every diagram is a kind edited as text). The slot's root carries rmk-editor, so the --rmk-diagram-* tokens you map on that class reach it wherever it sits, plus rmk-diagram-toolbar-host and data-rmk-diagram-toolbar="filled" or "empty" to style it by; the row inside keeps its inline look (.rmk-diagram-toolbar.is-detached) until you restyle it. If you don't mount a <DiagramToolbar>, nothing changes. The live editor uses this to put its row in the pill floating over the top-left corner of the canvas.

The live editor shares diagrams by link. The source, annotation included, is deflated into the URL hash as #pako:<base64url> (the same zlib form mermaid.live uses, with a #base64: fallback), so nothing gets uploaded. Its Share panel gives you the link, an "Open in visual editor" badge for a README, and an ?embed=1 iframe snippet that hides the site chrome. The format is tested in tests/mermaid-share.test.ts.

Only the /editor, /canvas and /client entries load React. A Node service that renders diagrams or resolves variables in them can import the root entry and skip installing React and Lexical.

Standalone canvas#

@react-markdown-kit/mermaid/canvas is the same canvas without a Markdown editor around it. You give it Mermaid source and it hands Mermaid source back. It fills whatever container you give it and floats its tools over the drawing, either down the left edge or along the top, similar to how Excalidraw lays out its canvas.

import { MermaidCanvas } from '@react-markdown-kit/mermaid/canvas'
import '@react-markdown-kit/mermaid/styles.css'


<div style={{ height: 600 }}>
  <MermaidCanvas value={source} onChange={setSource} />
</div>
import { createMermaidCanvas } from '@react-markdown-kit/mermaid/canvas'


const canvas = createMermaidCanvas(document.getElementById('diagram')!, {
  value: 'flowchart LR\n  a[Web] --> b[API]',
  onChange: (source) => save(source),
})
canvas.update({ zoom: 1.5 })
canvas.destroy()

Props: value / defaultValue / onChange (the Mermaid source, a flowchart's with its layout line), toolbar (left, the default, top or none), zoom, drawingStyle (clean or ink), align (center, the default, or start), readOnly, labels (UI strings by key), kinds, editors, fallbackTitle, onFocusChange, className, style, and children, which render over the surface if you want chrome of your own. The component keeps its own undo history (Cmd/Ctrl+Z, Shift+Cmd/Ctrl+Z, Ctrl+Y), and a value changed from outside counts as one more entry in it. A kind without a canvas shows its static render or its source, and a flowchart with lossy features is locked behind the same notice the block shows. createMermaidCanvas returns { getValue, setValue, update, destroy }. The entry loads React and ReactDOM but doesn't pull in Lexical or the editor package. --rmk-mermaid-canvas-background sets the page colour behind the grid and --rmk-mermaid-tools-top moves the tool island under chrome of your own.

Legacy JSON fences#

The ```diagram skeleton and ```drawing payload formats from @zuilib/text-editor are still read as flowcharts, so older documents render and open on the canvas. The first edit rewrites the block as ```mermaid.

Programmatic API#

There isn't a separate API beyond the plugin. The root entry exports mermaid(options?) with fallbackTitle (names an untitled diagram) and kinds (the registry), the kind factories flowchart() and sequenceDiagram(), MERMAID_KEYWORDS (every Mermaid type the detector recognises) and the types a kind of your own implements: DiagramKind, DiagramParse, DiagramParseError, DiagramProblem, DiagramRenderOptions, DiagramWriteOptions, RetainedLine. The parsers, renderers and writers aren't exported on their own.

A kind is a plain object: a name and keywords for detection, a label, a starter for the insert button, parse(source) returning a model with problems, retained lines and lossy features, an optional render(model, options) returning hast (absent means support source), and an optional write(model, options) that re-emits the retained lines.

On /editor, mermaid() also takes style (clean or hand-drawn ink), newBlockWidth, sourceEditor (false hides the textarea, so a block edited as text shows the preview and the problems only), align (start, the default, keeps a drawing at the top-left of its canvas, as a block sits in a document; center puts it in the middle of the room the canvas has, on both axes, and scales one taller than that room down to fit, the way a page-as-canvas editor shows a diagram, without moving its coordinates) and editors (the canvas component per kind name). The editor entry exports the types DiagramKindEditor, DiagramKindEditorProps and DiagramKindEditors for a canvas of your own, and <DiagramToolbar> (DiagramToolbarProps: editor, className, placeholder), the slot that takes the canvases' tool rows out of the blocks. The JSON Schemas for the layout annotation and the legacy payloads are exported for validating generated text.

Last updated on