@docs.plus/extension-mermaid
v2.3.0
Published
Tiptap extension that draws Mermaid code blocks as live SVG diagrams with Pele, loaded on demand
Maintainers
Readme
@docs.plus/extension-mermaid
Tiptap extension that draws Mermaid code blocks as live SVG diagrams. The source stays on top, and the diagram redraws under it as you type.
Diagrams are drawn by Pele, the Mermaid renderer by Steph Ango (@kepano), CEO of Obsidian. MIT licence.
Why use it
- The diagram follows the text. The SVG redraws 300 ms after you stop typing, for local and remote edits. A mistake names its line and column, and the last good diagram stays in view.
- Charts, not only flowcharts. Pele detects and draws 33 diagram types, such as sequence, Gantt, pie, radar, sankey, XY chart and mind map. See the Gallery.
- Drawn by Pele, loaded on demand. Your bundler emits Pele as separate chunks, and one chunk loads per diagram type. mermaid.js is not a dependency. Pele's speed claims and our own timing, with their limits, are in About Pele.
- Size limits measured on real phones. A diagram over 10,000 characters or 250 arrows (
edges) shows a message instead of drawing. Larger ones took more than 200 ms on a real Android phone or iPhone. The numbers are in Measured on real phones. - Readers see the diagram, writers see both. The Hide source button folds the text away. The
foldoption sets it up. Read-only editors start folded. Syntax and Copy sit beside it, in the head row above the source. - No new node, and a plain fence in storage. A diagram is a standard
codeBlockwithlanguage: 'mermaid'. The document holds amermaidfence, as on GitHub, GitLab and Obsidian, and never the SVG. Removing the extension leaves plain code blocks. Yjs merges the source per character. - Light and dark from your page. The default colours follow
color-scheme. Pele's--pele-*variables, or thethemeoption, set your own. - Safe defaults, with one gap. Pele escapes diagram text. Links and remote images are off, and size limits cap the work. The SVG is not sanitized unless you pass
sanitize, so pass it for sources you do not trust.
Install
npm install @docs.plus/extension-mermaidOr use pnpm add @docs.plus/extension-mermaid, yarn add @docs.plus/extension-mermaid, or bun add @docs.plus/extension-mermaid.
Requires @tiptap/core ^3.32.2 and @tiptap/pm ^3.32.2 (Tiptap 3.x). They are peer dependencies. Some package managers do not install peers for you, so this line is always safe to run: npm install @tiptap/core@^3.32.2 @tiptap/pm@^3.32.2. Keep every @tiptap/* package in your app on the same version.
Upgrading from 2.2.0 to 2.3.0? The Tiptap peer floor rose from ^3.31.3 to ^3.32.2. Move @tiptap/core, @tiptap/pm and your other @tiptap/* packages first, then update this package. Most apps need no other change. Two things behave differently for an app with its own codeBlock node view:
- A plugin
nodeViews.codeBlockthat ranks below this one now gets every block that is notmermaid. Itsupdatemust returnfalsewhen a block's language changes to or frommermaid. - An app with its own
addNodeViewoncodeBlockcan now see the development warning. To draw diagrams there, callmermaidNodeView.
The Changelog has the full list.
This package imports no React, Vue, or Next.js code. Use it from a plain page, Vite, React, Vue, Svelte, Next.js, Nuxt, or SvelteKit. Create the editor in the browser.
React Native has no DOM. Load the editor in a web view.
Installs one runtime dependency: pele (1.8.0, pinned exactly), the renderer. The package loads Pele with a dynamic import(), so your bundler emits it as separate chunks. Nothing loads from a CDN. For CSP, see Content Security Policy.
Import the stylesheet @docs.plus/extension-mermaid/styles.css. The fold control, the head and the actions need it.
Quickstart
Run this Quickstart in your browser first, with nothing to install. The app lives in examples/vanilla.
The snippet also imports @tiptap/starter-kit. Its codeBlock node holds each diagram. Add it with npm install @tiptap/starter-kit@^3.32.2 when your app has none yet.
Your page needs color-scheme: light dark in its CSS, such as :root { color-scheme: light dark; }. Without it, diagrams keep the light colours in dark mode.
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { Mermaid } from '@docs.plus/extension-mermaid'
import '@docs.plus/extension-mermaid/styles.css'
// The page needs <div id="editor"></div> and `color-scheme: light dark`.
const editor = new Editor({ extensions: [StarterKit, Mermaid] })
editor.mount(document.querySelector('#editor')!)
editor.commands.setContent(
'<pre><code class="language-mermaid">flowchart LR\n A[Write] --> B[See it]</code></pre>'
)You should see the source with a two-box flowchart under it. Edit the source, and the diagram redraws after you stop typing.
Caveats
- Only the exact language
mermaiddraws. A fence taggedMermaidwith a capital M stays plain code. Markdown keeps all the text after the backticks as the language, so```mermaid titlestays plain code. In HTML, that text becomes two classes, and the parser reads onlylanguage-mermaid. So the same block becomes a diagram after an HTML round trip. This follows from how the two parsers read the language, not from this package. Tag the fencemermaidalone. - The editor needs a
codeBlocknode. Without one, nothing draws andinsertMermaidreturns false. - A host
addNodeViewoncodeBlockwins. It replaces the diagram view, for exampleCodeBlockLowlight.extend({ addNodeView }). CallmermaidNodeViewin that view to keep diagrams. In development, the console warns once per editor:[extension-mermaid] Another node view owns codeBlock…. The check readsprocess.env.NODE_ENV, so without a globalprocessit stays off. The check changes only whether the warning shows. It never changes which view draws. - Among plugin views on
codeBlock, the first one wins. A higher extensionprioritycomes first. At equal priority, the extension listed last comes first. Soextensions: [Mermaid, Other]letsOthertake every block, and no diagram draws. A plugin view is anodeViews.codeBlockprop on a ProseMirror plugin. This view hands every block that is notmermaidto the next plugin view. A plugin view that comes before this one takesmermaidblocks too, so no diagram draws. In development, the console shows the same warning. To fix it, listMermaidafter the other extension, or giveMermaida higherpriority. Or callmermaidNodeViewin that plugin view through a wrapper, as Use it in a plugin view shows. - Without
styles.css, the fold control, the head and the actions render unstyled, and Pele's own error box shows. Add the stylesheet. Or setfold: false,helpLink: falseandcopy: false, and copy the three rules from Withoutstyles.css. - The default colours need
color-scheme. They uselight-dark(). Setcolor-scheme: light darkon your page, or the light colours always show. - Pele has no time limit.
limitsis the only cap on its work, so raise it with care.edgescounts arrows in flowchart and agentflow (a Pele diagram type) only. See Security. - The SVG is not sanitized by default. Pass
sanitizewhen sources come from people you do not trust. See Sanitizing the SVG.
The full list, with the reason for each, is in Caveats.
Common tasks
Add a toolbar or slash item
editor.chain().focus().insertMermaid().run()The command inserts a four-line flowchart and puts the caret at the end of the source. Gate the button with editor.can().insertMermaid(). It does not check that a diagram can draw. Read the active state with editor.isActive('codeBlock', { language: 'mermaid' }). See Commands.
Writers can also type ```mermaid and then a space or Enter. The code block input rule makes a diagram block, and it matches lowercase only. A Markdown mermaid fence loads as a diagram when your editor parses Markdown. This package does not load a Markdown parser. Install one with npm install @tiptap/markdown@^3.32.2, then add Markdown next to Mermaid:
import { Markdown } from '@tiptap/markdown'
const editor = new Editor({ extensions: [StarterKit, Mermaid, Markdown] })
editor.commands.setContent('```mermaid\nflowchart LR\n A --> B\n```', { contentType: 'markdown' })Change the size limits
Mermaid.configure({ limits: { characters: 20000 } })A missing field keeps its default, so edges stays 250 here. Each diagram block is checked on its own.
charactersis the source's JavaScriptlength, spaces andaccTitlelines included. A source of exactly the limit draws. Most emoji count 2, a flag counts 4, and a family emoji counts 11. Measure withsource.length.edgescounts each link Pele draws. A chainA --> B --> Cis 2.A --> B & Cis 2, andA & B --> C & Dis 4.<-->,-.->,==>,---and the invisible~~~count 1 each.- Only flowchart and agentflow have an edge count. Agentflow is a Pele diagram type. Other types, such as sequence, have only the
characterslimit. - A block over a limit shows "This diagram is too large to draw." and is never drawn.
onStateChangereportsstate: 'error'witherror.code'limit'. If the block drew before, the last good diagram stays, dimmed. The dimming needsstyles.css, or the third rule in Withoutstyles.css. - No limit covers the whole document. A limit has no
falsesetting.Infinitylifts it, and0blocks every diagram. Pele has no time limit, so lift or raise a limit only for sources you trust. - Pele has fixed caps of its own, such as the size of the drawn SVG. They also report
'limit', andlimitsdoes not change them.
See Options.
Sanitize the SVG
import DOMPurify from 'dompurify'
Mermaid.configure({
sanitize: (svg) =>
DOMPurify.sanitize(svg, {
USE_PROFILES: { svg: true },
ADD_ATTR: ['role'],
FORBID_TAGS: ['style', 'a', 'image', 'use'],
FORBID_ATTR: ['href', 'xlink:href'],
RETURN_DOM_FRAGMENT: true
})
})sanitize must return a DOM Node, so keep RETURN_DOM_FRAGMENT: true. A returned string shows as plain text, not as a diagram. The package does not depend on DOMPurify. The attributes that must survive are listed in Security.
Report slow renders and load errors
Mermaid.configure({
onStateChange({ state, error, durationMs }) {
if (state === 'load-error') console.warn('Mermaid renderer did not load', error?.message)
if (durationMs !== null && durationMs > 200) console.warn('Slow diagram', durationMs)
}
})With the built-in view, a block shows a spinner under the source while Pele loads. After a load-error, it shows "Could not load the diagram renderer." and a Try again button. durationMs covers Pele plus the sanitizer. It is null for every state except ready and error. See onStateChange.
Gallery
Each block is a plain mermaid fence, drawn with the default colours. Each screenshot has a light and a dark version, and follows your system preference.
Documentation
| Guide | What it covers |
| ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| API reference | Options, commands, states, onStateChange, fixed behaviour, class names, TypeScript exports, and the full caveats list |
| Guide | Theming, source folding, the view slot, accessible names, syntax highlighting, and your own code block view |
| Security | Untrusted sources, limits, sanitizing the SVG, and CSP |
| About Pele | What Pele is, how this package uses it, and the credit |
| Changelog | Every release and its breaking changes |
| For AI coding agents | This README as plain Markdown for your agent. The docs are also on Context7 |
Part of docs.plus
This extension is built for and maintained by docs.plus. docs.plus is a free, real-time collaboration tool that lets communities organize knowledge hierarchically, with a chat thread on every heading. docs.plus runs these packages from source in production, so every release is exercised there before it reaches npm.
- Website: docs.plus
- Project README: docs-plus/docs.plus
- Sibling extensions and recommended pairings: extensions/README.md
Contributing
Bug reports and PRs welcome. Setup, test commands, and the playground harness live in CONTRIBUTING.md.
License
MIT — see LICENSE.
