Skip to content

Repository files navigation

mermaid-lint 🧜‍♂️

Validate Mermaid diagrams embedded in Markdown files. Uses the official mermaid.parse() API — catches real syntax errors, not just missing diagram-type keywords.

jasonworden.com/mermaid-lint

npm version npm downloads CI License: MIT

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:

Invalid Mermaid diagram flagged inline in VS Code

Quick start

npx mermaid-lint            # validate every Mermaid block in your git-tracked Markdown

No 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:

Why mermaid-lint

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.

Packages

Package Published Description
@mermaid-lint/cli npm Command-line runner
@mermaid-lint/remark npm remark-lint plugin
@mermaid-lint/markdownlint npm markdownlint async custom rule
@mermaid-lint/textlint npm textlint rule (async)
@mermaid-lint/vitest npm Vitest adapter
@mermaid-lint/jest npm Jest adapter
@mermaid-lint/core npm Core utilities (extract, validate, discover) — API docs
mermaid-lint-vscode VS Code Marketplace Open VSX 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-vscode does not mirror that numbering: it is versioned independently, may skip npm package version numbers, and may ship a newer published @mermaid-lint/core without renaming itself to match. See packages/vscode/PUBLISHING.md.

CLI

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.

Beyond JavaScript projects

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.

Integration guides

Framework-specific setup notes live in docs/integrations/README.md.

GitHub Actions

- uses: jasonworden/mermaid-lint-action@v1
  with:
    files: 'docs/**/*.md **/*.mmd'
    strict: true

See mermaid-lint-action for full options and inline PR annotation support.

Configuration

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 / .mjs
  • package.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
  }
}

remark

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 errors

Or 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' } }]

Autofix

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.

markdownlint

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.

What this provides today

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.

Autofix (--fix)

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.

CLI / CI usage

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] };

VS Code (inline squiggles, no separate extension)

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.

textlint

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).

VS Code extension

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.

Vitest

// 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

Jest

// 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.

How it works

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
Loading
  • Discovery: git ls-files -- '*.md' '*.mdx' '*.markdown' '*.mmd' by default; --all falls back to recursive filesystem scan. Add extensions with --ext crv,foo or extensions: ['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 mermaid blocks — 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 the fences config option. Only .mmd files are treated as a single whole-file diagram — every other extension uses fenced-block extraction
  • Validation: Primary pass via @mermanjs/web WASM (Rust, ~3.7–4.4× faster). On any error, falls back to mermaid.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's error.raw. See docs/error-messages.md.

Semantic rules

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]
Loading
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.

Diagram types

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.

Performance

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.

Development

pnpm install
pnpm test                              # vitest (core + cli + vitest adapter)
pnpm --filter @mermaid-lint/jest test  # jest adapter
pnpm lint                              # biome

About

Validate Mermaid diagrams in Markdown! Catch real syntax errors using the official parser, built for CI.

Topics

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages