skip to content
Jason Worden
Table of Contents

A while back a teammate sent me a screenshot of a flowchart on our docs site rendering as a red Syntax error box. I’d pushed a wrong arrow two weeks earlier and nothing had caught it — tests green, build green. Mermaid diagrams are plain text in fenced code blocks, so there’s nothing to type-check or lint. They only fail when something tries to render them, and by then they’ve already shipped.

That’s the gap mermaid-lint closes.

What it does

mermaid-lint validates every Mermaid diagram in your Markdown using a two-tier pipeline — a Rust WASM fast path for speed, with the official mermaid.parse() API as the authoritative fallback. If the diagram can’t be parsed, the check fails with the file path, line number, and the actual error message from the parser.

What you get as of v0.35.0:

  • Finds every diagram. Scans .md, .mdx, .markdown, and .mmd files (plus any extra extensions you configure) with indentation-aware extraction — it catches diagrams in indented fences (inside list items, callouts, and other indented structures), not just top-level fences. Handles both backtick and CommonMark tilde (~~~) fences.
  • Semantic rules, not just syntax. Beyond parsing, it runs a set of semantic checks for diagrams that parse but still mislead — the legacy graph keyword, a flowchart with no direction, duplicate or self-looping edges, empty labels, a sequenceDiagram activate with no matching deactivate, duplicate class methods, and more. By v0.35.0, that coverage extends into newer and experimental Mermaid families too — journey, timeline, C4Context, and the newer beta-prefixed syntax for architecture-beta, packet-beta, sankey-beta, and xychart-beta diagrams — so the tool keeps paying off after you outgrow the usual flowchart-only docs. Each rule has a configurable severity (off / warn / error, à la Biome); duplicate-ids — a node re-declared with a conflicting label, which Mermaid silently drops — defaults to a hard error. Silence one inline with %% mermaid-lint-disable <rule>, or pass --no-semantic to skip them all.
  • Zero-config, configurable when you need it. Works with no setup, but reads a mermaid-lint.config.js, .mermaidlintrc, or a mermaidLint key in package.json when present — to scan custom file extensions, restrict which fence styles count, tune per-rule severity, or treat every warning as an error. CLI flags always override config.
  • Git-aware by default. Only checks tracked files, so a diagram you’re still drafting doesn’t fail CI until you commit it. Pass --include/--exclude globs to narrow scope, or --no-gitignore to check everything.
  • Built for CI and tooling. --format json gives machine-readable output for CI annotations, editor integrations, or custom pipelines. Colored human output respects NO_COLOR and non-TTY. A GitHub Action is available if you’d rather skip the install entirely — it posts inline PR annotations at the exact failing line.
  • stdin support. Pipe diagrams directly: cat diagram.md | mermaid-lint -. Works with --format json too.
  • Auto-fix mechanical errors. --fix rewrites files in place: normalizes -> to --> inside flowcharts, inserts a missing colon-space separator in sequence diagram messages, and closes unclosed fences. Multi-pass up to convergence, always idempotent. Pipe mode (mermaid-lint --fix -) writes fixed content to stdout.
  • remark / unified plugin. @mermaid-lint/remark is a unified-lint-rule plugin for validating Mermaid blocks inside remark pipelines — an Astro site configured with @astrojs/markdown-remark, docusaurus, MDX, or anything else built on unified. Errors surface as VFile messages with the absolute document line number.
  • Rust speed, zero native install. The fast path is a real Rust parser (merman) compiled to WebAssembly and shipped inside the npm package — so you get Rust-grade throughput (~0.1 ms/diagram) with no Rust toolchain, no native binary to build, and no platform-specific prebuilds. It runs anywhere Node does.
  • Pure Node runtime. No headless browser, no Puppeteer, no CDN dependency — the WASM module and the mermaid.parse() fallback both run in-process. ESM-native, Node ≥ 20.

It ships as a small family of packages so you adopt exactly the layer you need:

Pick your lint lane

If you already run markdownlint, use the markdownlint adapter and let one content-lint command own both Markdown style and Mermaid blocks:

import mermaid from "@mermaid-lint/markdownlint";
export default {
globs: ["README.md", "docs/**/*.md", "src/content/**/*.md"],
customRules: mermaid,
config: { default: true },
};

That’s what this blog uses now. A broken ```mermaid fence fails the same pnpm lint:markdown gate as a malformed Markdown table, so content hygiene stays in one place.

If you already have a test runner and want each diagram reported as its own test case, use the Vitest drop-in instead:

import { defineMermaidTests } from "@mermaid-lint/vitest";
defineMermaidTests();

Two lines. That discovers every Mermaid block in every git-tracked Markdown file, runs the real parser over each one, and gives each diagram its own named test case with the source file and line number baked into the test name. When something breaks, the report points straight at the file and line. No registry to maintain: any new diagram in any tracked file is covered automatically.

Not using a test runner? The CLI works the same way:

Terminal window
npx @mermaid-lint/cli # check git-tracked files
npx @mermaid-lint/cli "docs/**/*.md" # or a glob
npx @mermaid-lint/cli --all --format json # whole tree, machine-readable

GitHub Actions

If you’d rather skip the test runner entirely, there’s now a first-class Action:

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

Drop it into any workflow step and it validates diagrams with the same two-tier pipeline — no install, no config file needed. It also posts inline PR annotations so a broken diagram surfaces as a file-level comment pointing at the exact line, not just a red CI badge you have to dig into.

remark / unified

If you’re already in a unified pipeline — or in Astro with @astrojs/markdown-remark configured as your Markdown processor — you can wire mermaid-lint in as a lint plugin instead of adding a separate CI step:

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 mermaid validation errors

Or in .remarkrc.mjs:

export default {
plugins: ["remark-lint", "@mermaid-lint/remark"],
};

Errors are reported as VFile messages with the absolute line number of the failing diagram in the source file. Pass { strict: true } to elevate semantic warnings (e.g. a flowchart with no direction) to errors.

markdownlint and textlint

remark isn’t the only lint pipeline. If your docs linting already runs through markdownlint or textlint, there’s a plugin for each:

  • @mermaid-lint/markdownlint — an async custom rule. Add it to markdownlint-cli2 (or the markdownlint VS Code extension) and broken diagrams surface right alongside your existing Markdown lint failures.
  • @mermaid-lint/textlint — a textlint rule. (textlint awaits async rules, so it runs the real merman + mermaid.parse() validator — something an ESLint rule can’t do, since ESLint rules must be synchronous.)

All of these — CLI, test drop-in, remark, markdownlint, textlint — delegate extraction, validation, and line mapping to a shared Markdown adapter in @mermaid-lint/core. Same diagnostics, same line numbers, wherever you wire it in.

In your editor

The shortest feedback loop is the editor itself. The Mermaid Lint VS Code extension (also on Open VSX for Cursor, VSCodium, and Windsurf) puts red squiggles under invalid Mermaid as you type — in ```mermaid blocks inside .md files and in standalone .mmd files — with Problems-panel entries, hover messages, and Cmd . quick-fixes that apply the same mechanical --fix corrections without leaving the editor.

It’s powered by @mermaid-lint/core, the same engine as the CLI, so what you see while editing is exactly what CI enforces — you catch the broken arrow before you commit it, not after the build goes red.

Rust WASM + JS/TS: fast and accurate

Here’s the engineering tradeoff that makes this interesting.

Pure JavaScript validation — running mermaid.parse() through jsdom on every diagram — is authoritative but slow. The jsdom + mermaid.js stack costs ~400 ms to initialize even for a single diagram. For a repo with hundreds of diagrams, that adds up quickly.

The fast alternative is a Rust-based parser compiled to WASM. @mermanjs/web is a Rust implementation of the Mermaid grammar, and it’s much faster: ~100 ms one-time init, then ~0.1 ms per diagram. That’s a 3–4× speedup on a valid corpus.

But Rust and JavaScript parsers can drift. A Rust parser might reject something mermaid.js accepts (false positive — your CI breaks on valid diagrams) or accept something mermaid.js rejects (false negative — broken diagrams slip through). Either way, you lose trust in the tool.

v0.5.0 solves this with a two-tier pipeline and a CI-enforced parity harness:

The pipeline:

  1. merman WASM first — validates each diagram in ~0.1 ms. If it says valid, we return immediately.
  2. mermaid.js as authoritative fallback — only loads when merman signals an error. If mermaid.js accepts a diagram merman rejected, we call it valid. No false positives from parser divergence. When mermaid.js also rejects, it supplies the precise line/col error location.
flowchart LR
  A["discover files"] --> B["extract blocks"]
  B --> C["merman WASM"]
  C -->|valid| PASS[pass]
  C -->|invalid| D["mermaid.parse()"]
  D -->|parses| PASS
  D -->|throws| FAIL["fail: file:line + message"]

A flowchart in Mermaid

The parity harness:

On every PR, a test suite runs a corpus of 24+ valid and 10+ invalid diagrams — spanning all 19 supported diagram types (flowchart, sequence, class, state, ER, pie, Gantt, git graph, and more) — against both parsers. If merman ever accepts a diagram that mermaid.js rejects, CI fails. This isn’t a one-time check; it’s enforced on every change.

The result: Rust WASM speed on the happy path (valid diagrams in CI) with a guarantee that the fast path never silently passes a broken diagram.

Benchmarks on Apple M4 Max:

Diagramsv0.3.0v0.5.0Speedup
50407 ms121 ms3.4×
200553 ms159 ms3.5×
1,0001018 ms260 ms3.9×
10,0006643 ms1699 ms3.9×
100,00062734 ms15590 ms4.0×

Why this matters for agentic engineering

There’s a less obvious reason to keep diagrams valid that didn’t exist a few years ago: AI coding agents.

In agentic engineering workflows — where AI agents are reading your docs, parsing your architecture diagrams, and generating code alongside you — your Markdown files are live context. A broken diagram doesn’t just fail a human reader; it injects a parse error into a context window. An agent trying to understand a system from a flowchart that renders as Syntax error is flying blind.

mermaid-lint keeps your documentation clean for both audiences. The same one-liner that catches a fat-fingered arrow before it ships also keeps the signal quality high for every agent that reads your repo.

What it won’t do

mermaid-lint validates syntax and runs a set of semantic rules — legacy keywords, missing directions, duplicate or self-looping edges, dangling sequence activations, and more — but it still can’t tell whether your diagram is true. A flowchart that parses cleanly and trips no rule, yet describes an architecture your team moved away from six months ago, will pass — because no linter knows your codebase changed. That’s a code-review problem, not a linter one.

Project arc

The project has grown in layers: first a CLI and shared parser core, then faster validation, then integrations for the places documentation already gets checked, then a broader semantic layer that now reaches into the newer and more experimental corners of Mermaid while also adding docs-consistency guardrails.

timeline TD
  title mermaid-lint
  v0.1 : CLI + core Markdown extraction
  v0.5 : Rust WASM fast path + JS fallback parity harness
  v0.6 : GitHub Action
  v0.10 : markdownlint + remark support
  v0.11 : VS Code extension
  v0.17 : configurable semantic rules
  v0.35 : expanded semantics + docs consistency guards for newer Mermaid diagrams

A timeline in Mermaid

For a more detailed release-by-release view, see the mermaid-lint release history.

This post is a test case

This post is a Markdown file in an Astro blog. The two Mermaid diagrams above — the flowchart and the timeline — live as fenced code blocks inside that .md file, same as they would in any documentation repo. The moment it was committed, mermaid-lint started running against it on every CI build. If either diagram were malformed, this post would not have shipped.

That’s the whole point: Markdown is where diagrams live in practice, and mermaid-lint treats every fenced ```mermaid block in every tracked .md file as something that must parse. One line of config, the real parser, every diagram in every post covered for free — including this one.

If you keep diagrams in your repo, give it a try.