Validate Mermaid diagrams embedded in Markdown files. Uses the official mermaid.parse() API — catches real syntax errors, not just missing diagram-type keywords.
⚡ Fast by default — a Rust/WASM parser validates the happy path in ~0.1 ms/diagram; the heavyweight pure-JS mermaid.parse() path loads only when a diagram actually errors. See the benchmarks
Catches real syntax errors as you type — here the VS Code extension flagging an unterminated edge label in a .mmd file:
npx mermaid-lint # validate every Mermaid block in your git-tracked MarkdownNo install, no config. mermaid-lint discovers your .md / .mdx / .markdown / .mmd files, validates every ```mermaid block, and reports the precise line and column of any error:
docs/architecture.md:42:5: error: sequence message is missing a colon (sequenceDiagram)
The exit code is non-zero on failure, so it drops straight into CI or a pre-commit hook. From here:
- Editor — VS Code extension: live squiggles as you type, for both
```mermaidblocks and standalone.mmdfiles - Tests — Vitest / Jest adapters: fail your test run on a broken diagram
- Lint pipeline — remark · markdownlint · textlint
- CI — GitHub Action with inline PR annotations
- Library —
@mermaid-lint/core: the programmatic API, with full reference docs at docs.mermaidlint.com
mermaid-lint gives you actual Mermaid validation anywhere you write diagrams:
- 💥 Catches Mermaid syntax errors across all 27 diagram types
- 📍 Reports the precise line and column of the error
- 🧠 Adds opt-in semantic warnings like duplicate node IDs
- 🔧 Auto-fixes mechanical issues with
--fix - 🌈 Shows editor squiggles as you type in VS Code
- 🚀 Fits into CI, pre-commit hooks, and existing lint pipelines with one command
Plain Markdown linters don't validate diagram bodies — but mermaid-lint plugs into the ones you already run: markdownlint, remark, and textlint all gain Mermaid validation via a mermaid-lint rule.
| Package | Published | Description |
|---|---|---|
@mermaid-lint/cli |
Command-line runner | |
@mermaid-lint/remark |
remark-lint plugin | |
@mermaid-lint/markdownlint |
markdownlint async custom rule | |
@mermaid-lint/textlint |
textlint rule (async) | |
@mermaid-lint/vitest |
Vitest adapter | |
@mermaid-lint/jest |
Jest adapter | |
@mermaid-lint/core |
Core utilities (extract, validate, discover) — API docs | |
mermaid-lint-vscode |
VS Code extension — live squiggles in Markdown (.md, .markdown) blocks + standalone .mmd files |
Versioning note: the published
@mermaid-lint/*npm packages move in lockstep and share one version.mermaid-lint-vscodedoes not mirror that numbering: it is versioned independently, may skip npm package version numbers, and may ship a newer published@mermaid-lint/corewithout renaming itself to match. Seepackages/vscode/PUBLISHING.md.
Exit codes: 0 = all valid · 1 = validation failures (or warnings with --strict) · 2 = usage/IO error
See docs/cli.md for discovery modes, glob flags, stdin, JSON
output, strict mode, semantic toggles, and --fix examples.
JSON output (--format json) is documented in
docs/json-output.md — the full schema, field reference,
and a CI-scripting example.
For a selective project arc instead of a full changelog, see Release history.
mermaid-lint only requires Node.js ≥22 and runs via npx, so it works in any
project regardless of language. See docs/ci-and-precommit.md
for Python/Go/Rust recipes, pre-commit hooks (pre-commit, husky + lint-staged),
and CI usage.
Framework-specific setup notes live in docs/integrations/README.md.
- uses: jasonworden/mermaid-lint-action@v1
with:
files: 'docs/**/*.md **/*.mmd'
strict: trueSee mermaid-lint-action for full options and inline PR annotation support.
mermaid-lint auto-discovers a config file in your project root. Supported names (in priority order):
mermaid-lint.config.js/.cjs/.mjs.mermaidlintrc/.mermaidlintrc.json.mermaidlintrc.js/.cjs/.mjspackage.json→"mermaidLint"field
CLI flags always override config values. A starter template is provided at mermaid-lint.config.example.js.
// mermaid-lint.config.js
export default {
// Glob patterns to validate (used when no CLI paths given and --all not set)
files: ['docs/**/*.md', '**/*.mmd'],
// Glob patterns to exclude
ignore: ['node_modules/**', 'dist/**'],
// Treat semantic warnings as errors — equivalent to --strict
strict: false,
// false disables ALL semantic rules — equivalent to --no-semantic
semantic: true,
// Per-rule severity ('off' | 'warn' | 'error'), layered over the defaults.
// Most rules default to 'warn'; 'duplicate-ids' and
// 'frontmatter-must-be-first' default to 'error'.
rules: {
'prefer-flowchart': 'warn', // legacy `graph` keyword → prefer `flowchart`
'require-direction': 'warn', // `flowchart`/`graph` with no direction (defaults to TD)
'no-experimental': 'warn', // `*-beta` diagram types (unstable syntax)
'duplicate-ids': 'error', // same node id, conflicting labels (wrong output)
},
// 'text' (default) or 'json'
format: 'text',
// Code-fence markers to recognize. Defaults to both, matching CommonMark:
// 'backtick' → ```mermaid … ```
// 'tilde' → ~~~mermaid … ~~~
// Restrict to ['backtick'] to ignore tilde fences.
fences: ['backtick', 'tilde'],
};Or as JSON in .mermaidlintrc.json:
{
"files": ["docs/**/*.md"],
"ignore": ["dist/**"],
"strict": true
}Or inline in package.json:
{
"mermaidLint": {
"ignore": ["dist/**"],
"strict": true
}
}import { remark } from 'remark';
import remarkLint from 'remark-lint';
import remarkLintMermaid from '@mermaid-lint/remark';
const result = await remark()
.use(remarkLint)
.use(remarkLintMermaid)
.process(markdown);
// result.messages contains any mermaid validation errorsOr in .remarkrc.mjs to run from the command line (npx remark --frail .):
export default {
plugins: [
'remark-lint',
'@mermaid-lint/remark',
]
};Enable strict mode (treat semantic warnings as errors):
export default {
plugins: [
'remark-lint',
['@mermaid-lint/remark', { strict: true }],
]
};Tune individual rules with rules (same shape as the CLI's rules
config) — e.g. enable an off-by-default rule or silence one:
['@mermaid-lint/remark', { rules: { 'no-orphan-nodes': 'error', 'no-self-loop': 'off' } }]remark has no lint-fixer API, so fixing ships as a separate transformer,
remarkMermaidFix, alongside the report-only lint rule. It applies the same
mechanical corrections as the CLI's --fix (normalize -> arrows, add
missing sequence-message colons; never semantic changes):
import { remark } from 'remark';
import remarkLintMermaid, { remarkMermaidFix } from '@mermaid-lint/remark';
const result = await remark()
.use(remarkLintMermaid) // report
.use(remarkMermaidFix) // fix
.process(markdown);A transformer only takes effect when remark serializes, so fixes apply under
npx remark file.md --output (and are inert in pure-lint runs). remark --output
already reserializes the whole document via remark-stringify on every run; the
fixer changes only the Mermaid fence bodies within that.
A set of markdownlint async custom
rules that validate Mermaid blocks as part of your existing markdownlint run — in
CI, on the command line, and inline in VS Code. There's one rule per check
(mermaid-syntax for parse errors, mermaid-no-self-loop, mermaid-duplicate-ids,
…); the default export is the recommended bundle, and all/individual rules let
you opt into more or cherry-pick. See the
package README for the full rule list.
| Surface | Supported | Notes |
|---|---|---|
```mermaid blocks in Markdown (.md, .markdown, …) |
✅ | CLI, CI, and in-editor squiggles |
Standalone .mmd diagram files |
❌ | markdownlint only processes Markdown; it never invokes the rule on .mmd. Use the VS Code extension for .mmd coverage in the editor. |
| Zero-config editor setup | ❌ | requires the steps below (npm install + setting + workspace trust) |
Autofix via markdownlint-cli2 --fix |
✅ | mermaid-syntax applies the same mechanical corrections as the CLI's --fix (normalize -> arrows, add missing sequence-message colons). Semantic rules never autofix. |
The mermaid-syntax rule wires Mermaid into markdownlint's native autofix, so
markdownlint-cli2 --fix corrects the mechanical mistakes inside your diagram
blocks alongside your other Markdown fixes:
npx markdownlint-cli2 --fix "**/*.md"It applies exactly the corrections the CLI's --fix does — normalizing
flowchart arrows (-> → -->) and inserting missing sequence-message colons.
These are meaning-preserving; semantic findings (self-loops, duplicate ids, …)
are reported but never auto-changed. Closing an unclosed fence remains CLI-only.
npm install --save-dev @mermaid-lint/markdownlint markdownlint-cli2// .markdownlint-cli2.mjs
export default {
config: { default: true },
customRules: ['@mermaid-lint/markdownlint'],
};Run it: npx markdownlint-cli2 "**/*.md". Use markdownlint-cli2 >= 0.17.0 —
earlier versions bundle a markdownlint older than 0.37, which predates async
custom rules, so the rules are silently skipped (zero errors reported).
To enable every check (including the higher-false-positive no-orphan-nodes and
prefer-explicit-participants), spread the all bundle:
import mermaid from '@mermaid-lint/markdownlint';
export default { config: { default: true }, customRules: [...mermaid.all] };Install the markdownlint extension
(v0.50+; it bundles a recent markdownlint-cli2, so async rules run), add the
package to your workspace (npm i -D @mermaid-lint/markdownlint), then in
.vscode/settings.json:
{
"markdownlint.customRules": ["./node_modules/@mermaid-lint/markdownlint"]
}You must trust the workspace — the extension blocks custom-rule JavaScript in
untrusted workspaces. Invalid ```mermaid blocks in .md files then get
inline diagnostics as you type. (.mmd files are not covered — see the table
above.)
Requires markdownlint >= 0.37.0 for async custom rule support.
A textlint rule that validates ```mermaid
blocks as part of a textlint run. textlint awaits a Promise returned from a rule,
so — unlike ESLint, whose rules are synchronous — it runs the full async
validator (merman + mermaid.js), the same engine the CLI uses.
npm install --save-dev textlint @textlint/textlint-plugin-markdown @mermaid-lint/textlint// .textlintrc.js
module.exports = {
plugins: ['@textlint/markdown'],
rules: {
'@mermaid-lint/textlint': true,
},
};Run it: npx textlint "**/*.md". Pass { strict: true } to also report semantic
warnings (e.g. duplicate node IDs):
rules: {
'@mermaid-lint/textlint': { strict: true },
},Or pass rules (same shape as the CLI's rules config) to
enable an off-by-default rule or silence one:
rules: {
'@mermaid-lint/textlint': { rules: { 'no-orphan-nodes': 'error' } },
},The rule is also a textlint fixer, so textlint --fix applies the same
mechanical corrections as the CLI's --fix (normalize -> arrows,
insert missing sequence-message colons) inside your Mermaid blocks:
npx textlint --fix "**/*.md"These corrections are meaning-preserving; semantic findings are reported but never auto-changed. (List-indented fences are a no-op — textlint de-indents the block body; use the CLI for those.)
Why textlint and not ESLint? ESLint rules must be synchronous, so they cannot run Mermaid's async parser. See the parsing-vs-linting explainer and tracking issues #39 (ESLint) and #38 (Biome).
A dedicated extension (mermaid-lint-vscode, in packages/vscode)
validates Mermaid as you type, including Markdown fences and standalone .mmd
files. It reports inline diagnostics, hover messages, Problems-panel entries,
and quick-fix code actions while honoring the same mermaid-lint config as the
CLI.
Install it from the
VS Code Marketplace
or Open VSX,
or run code --install-extension mermaid-lint.mermaid-lint-vscode.
The extension's version is independent from the lockstep @mermaid-lint/* npm
package version. An extension release may therefore lag, skip, or package a
newer published core version without using the same version number as npm.
// mermaid.test.ts
import { defineMermaidTests } from '@mermaid-lint/vitest'
defineMermaidTests() // auto-discovers git-tracked *.md
defineMermaidTests({ root: '/my/docs' }) // explicit root
defineMermaidTests({ strict: true }) // also fail on semantic warnings// mermaid.test.mjs
import { defineMermaidTests } from '@mermaid-lint/jest'
defineMermaidTests()Requires NODE_OPTIONS=--experimental-vm-modules (Jest + native ESM).
Both fail a test on any syntax error or error-severity semantic finding (e.g. a duplicate id); pass strict: true to also fail on warnings, or rules to tune individual checks. Need the results without registering tests? Call lintMermaidFiles(opts). Full options in the vitest / jest READMEs.
flowchart LR
src[".md / .mdx / .mmd files"]
discover["discoverFiles()"]
extract["extractMermaidBlocks()"]
validate["validateBlock()"]
ok(["✓ valid"])
err(["✗ error + location"])
src --> discover
discover --> extract
extract --> validate
validate --> ok
validate --> err
- Discovery:
git ls-files -- '*.md' '*.mdx' '*.markdown' '*.mmd'by default;--allfalls back to recursive filesystem scan. Add extensions with--ext crv,fooorextensions: ['crv']in config to discover other fenced-Markdown file types (e.g. Carve.crv). Files you name explicitly are always linted, whatever their extension. - Extraction: Parses CommonMark fenced
mermaidblocks — backtick (```mermaid) and tilde (~~~mermaid) markers, variable-length fences (4+ chars, so a body can contain```), CRLF, indentation, info-strings, and unclosed fences. Restrict recognized markers with thefencesconfig option. Only.mmdfiles are treated as a single whole-file diagram — every other extension uses fenced-block extraction - Validation: Primary pass via
@mermanjs/webWASM (Rust, ~3.7–4.4× faster). On any error, falls back tomermaid.parse()via jsdom for precise line/col locations and authoritative verdict - Error messages: where the defect matches a recognized shape (a missing sequence-message colon, a single-dash flowchart arrow, an unclosed node shape, …), the reported message names it directly instead of echoing mermaid's grammar-token wording, and includes a corrected-line suggestion when the fix is mechanical. mermaid's original message is still available via
--format json'serror.raw. See docs/error-messages.md.
In addition to syntax errors, mermaid-lint runs semantic rules over diagrams
that mermaid.parse() accepts but which are legacy, ambiguous, or render
incorrectly. Each rule has a per-rule severity (off | warn | error), and
you can tune any rule via the rules config key.
See docs/semantic-rules.md for the full rule table, default severities, scopes, and example output.
Suppress rules with directives. A reason after : is required.
flowchart LR
A[Start] --> B[End]
%% mermaid-lint-disable-next-line duplicate-ids: ids collide upstream
A[Also Start] --> C[End]
| Directive | Scope |
|---|---|
%% mermaid-lint-disable-next-line <rules>: <reason> |
the next line |
%% mermaid-lint-disable <rules>: <reason> … %% mermaid-lint-enable <rules> |
from the directive to the matching enable, or end of diagram |
%% mermaid-lint-disable-diagram <rules>: <reason> |
the whole diagram |
<!-- mermaid-lint-disable-file <rules>: <reason> --> |
every diagram in the Markdown file |
<rules> is a space- or comma-separated list of rule ids, or all.
Directives must sit below any YAML frontmatter. Mermaid only recognizes frontmatter at the very start of a diagram, so a comment above it silently breaks rendering:
---
title: My Diagram
---
%% mermaid-lint-disable-next-line duplicate-ids: ids collide upstream
flowchart LR
A[Start] --> B[End]
The frontmatter-must-be-first rule catches this placement mistake for you.
Note that it cannot be suppressed with a %% directive above the frontmatter —
that comment is itself the thing that breaks rendering. Use a file-scope
<!-- mermaid-lint-disable-file frontmatter-must-be-first: reason -->, which
sits outside the diagram body.
all covers semantic rules only, and never the three suppression meta-rules
below — suppression-unknown-rule, suppression-unused, and
suppression-malformed stay on even under -disable-diagram all; name one
explicitly to quiet it. Suppressing a syntax error requires naming mermaid
explicitly too, and only at diagram or file scope:
%% mermaid-lint-disable-diagram mermaid: uses syntax our pinned parser predates.
Structural errors are never suppressible. An unclosed ```mermaid fence
or an empty block is a defect in the Markdown, not the diagram — and an
unclosed fence has no parseable body for a %% directive to live in, so
-disable-file mermaid would be the only lever and would hide broken Markdown
indefinitely. mermaid suppression applies to diagrams the parser rejected,
not to fences that never closed.
Malformed and unknown directives are themselves reported, and so is a directive
that suppressed nothing — see suppression-malformed,
suppression-unknown-rule, and suppression-unused in
docs/semantic-rules.md. A body-scope directive
(-disable-next-line/-disable/-disable-diagram) is judged against its own
diagram; a file-scope (<!-- -->) one against the whole document, and is
reported once, at the comment's own line, only if no diagram in the file used
it.
File-scope directive diagnostics need a whole-document view, so today only the VS Code extension surfaces them. The CLI, remark, textlint, and the test-runner adapters validate one diagram at a time and report body-scope directives only — they still honor a file directive, they just never tell you one is broken or stale.
markdownlint caveat. markdownlint blanks out the contents of HTML comments before handing a document to custom rules, so the markdownlint integration cannot see
<!-- -->directives at all — a file-scope suppression neither applies nor gets reported there. Use a%%body-scope directive instead when linting through markdownlint.
Disable everything for a run with --no-semantic.
mermaid-lint validates all 27 Mermaid diagram types using the official mermaid.parse() API. Some alternative linters (e.g. maid) only validate 5 types and silently pass all input for the other 22 (gantt, erDiagram, journey, mindmap, gitGraph, etc.). Every type in the table below is actively validated — none are pass-through.
Syntax validation is inherited from the bundled parser, so newly released diagram types work as soon as the mermaid dependency carries them. Semantic rules are hand-written per type, so the Related rules column is the honest measure of how much analysis a given type gets beyond "does it parse".
| Type | Keyword | Supported | Related rules | Notes |
|---|---|---|---|---|
| Flowchart | flowchart / graph |
✅ | duplicate-ids, prefer-flowchart, require-direction, no-duplicate-edges, no-self-loop, no-empty-labels, no-orphan-nodes, no-duplicate-node-declarations, click-target-not-found |
graph is an alias for flowchart |
| Sequence | sequenceDiagram |
✅ | no-activate-without-deactivate, prefer-explicit-participants, sequence-duplicate-participant |
|
| Class | classDiagram |
✅ | class-duplicate-class, no-duplicate-methods |
|
| State | stateDiagram-v2 |
✅ | state-duplicate-state, state-duplicate-transition, state-empty-composite, state-self-transition |
|
| Entity-Relationship | erDiagram |
✅ | er-duplicate-attribute, er-duplicate-entity, er-standalone-entity |
|
| Pie chart | pie |
✅ | pie-duplicate-label, pie-zero-value, pie-no-data |
|
| Gantt | gantt |
✅ | gantt-duplicate-task-id, gantt-undefined-dependency, gantt-empty-section, click-target-not-found |
|
| Git graph | gitGraph |
✅ | gitgraph-duplicate-commit-id, gitgraph-duplicate-tag, gitgraph-no-commits |
|
| User journey | journey |
✅ | journey-empty-section, journey-score-out-of-range, journey-task-without-actor, journey-no-tasks |
|
| Mindmap | mindmap |
✅ | mindmap-duplicate-sibling, mindmap-no-nodes, mindmap-deep-nesting |
|
| Quadrant chart | quadrantChart |
✅ | quadrant-duplicate-point, quadrant-no-points, quadrant-missing-x-axis, quadrant-missing-y-axis, quadrant-duplicate-quadrant |
|
| Requirement | requirementDiagram |
✅ | requirement-duplicate-name, requirement-duplicate-id, requirement-undefined-reference |
|
| C4 Context | C4Context |
✅ | c4-duplicate-id, c4-undefined-relationship-endpoint, c4-undefined-element-style, c4-undefined-relationship-style-endpoint |
|
| Timeline | timeline |
✅ | timeline-empty-section, timeline-empty-event, timeline-no-entries |
|
| XY chart | xychart-beta |
✅ | no-experimental, xychart-missing-x-axis, xychart-missing-y-axis, xychart-no-series, xychart-series-length-mismatch |
Experimental |
| Sankey | sankey-beta |
✅ | no-experimental, sankey-non-positive-value, sankey-duplicate-link, sankey-self-loop |
Experimental; duplicate-link keys on repeated source,target pairs regardless of value |
| Block | block-beta |
✅ | no-experimental, block-no-blocks |
Experimental |
| Packet | packet-beta |
✅ | no-experimental, packet-no-fields, packet-empty-labels |
Experimental |
| Architecture | architecture-beta |
✅ | no-experimental, architecture-no-elements, architecture-no-edges, architecture-duplicate-edge |
Experimental |
| Kanban | kanban |
✅ | kanban-duplicate-column, kanban-duplicate-task-id, kanban-empty-column, kanban-no-columns |
Columns and cards share one id namespace |
| Event modeling | eventmodeling |
✅ | eventmodeling-undefined-frame, eventmodeling-duplicate-frame-id, eventmodeling-invalid-flow |
|
| Radar | radar-beta |
✅ | no-experimental, radar-no-curves, radar-curve-length-mismatch, radar-duplicate-axis |
Experimental |
| Treemap | treemap-beta |
✅ | no-experimental, treemap-zero-value, treemap-no-leaves, treemap-duplicate-sibling, treemap-branch-with-value |
Experimental |
| Venn | venn-beta |
✅ | no-experimental, venn-duplicate-set, venn-non-positive-size, venn-single-set, venn-self-union, venn-no-sets, venn-duplicate-union |
Experimental; set/union declarations are never deduplicated |
| Ishikawa | ishikawa-beta |
✅ | no-experimental, ishikawa-no-causes, ishikawa-empty-category, ishikawa-deep-nesting, ishikawa-duplicate-sibling |
Experimental; one problem node, and every later line nests under it |
| Wardley map | wardley-beta |
✅ | no-experimental, wardley-undefined-component, wardley-orphan-component, wardley-no-components, wardley-mixed-coordinate-scale, wardley-duplicate-component |
Experimental; not covered by the Rust fast path — always falls back to mermaid.js |
| Tree view | treeView-beta |
✅ | no-experimental, treeview-no-nodes, treeview-duplicate-sibling |
Experimental; indented or ├── box-drawing form, node labels quoted or bare (bare needs mermaid ≥ 11.16) |
| ZenUML | zenuml |
❌ | - | Requires separate @mermaid-js/mermaid-zenuml package; not bundled in mermaid v11 |
The last eight rows need mermaid 11.15.0, so @mermaid-lint/core pins that
version exactly rather than accepting a range. Five of them — venn-beta,
ishikawa-beta, wardley-beta, treeView-beta, and eventmodeling — do not
exist in earlier 11.x releases, where mermaid rejects them with "No diagram
type detected". Under a floating range a consumer could resolve an older
mermaid and see valid diagrams reported as errors, so the pin is what makes the
table above a guarantee instead of an observation.
no-experimental picks up every *-beta type automatically, so those warn
without any per-type wiring.
The Rust/WASM fast path avoids the fixed mermaid.js + jsdom startup cost for valid diagrams, while mermaid.js remains the authoritative fallback for parser errors and precise line/column diagnostics.
See docs/performance.md for benchmarks, parser-accuracy checks, and reproduction steps.
pnpm install
pnpm test # vitest (core + cli + vitest adapter)
pnpm --filter @mermaid-lint/jest test # jest adapter
pnpm lint # biome