mermaid-flow-player
v2.0.0
Published
Animate Mermaid-rendered diagrams with semantic node/edge steps and scenarios
Maintainers
Readme
mermaid-flow-player
Step through your Mermaid diagrams. Add one script tag and every diagram on the page gets play, pause and step controls, narration, and a URL that links to a single step.
<script src="https://cdn.jsdelivr.net/npm/mermaid-flow-player/auto.global.js"></script>
<div class="mermaid">
flowchart TD
A[Validate token] --> B[Fetch user] --> C[Render page]
</div>Save that as a file, open it in a browser, and it plays. Your markup does not change. Mermaid loads itself if it is not already on the page, and the CSS is bundled and injected.
Where your diagrams already live
Auto mode upgrades three shapes of markup with no configuration:
| Your markup | Emitted by |
|---|---|
| <div class="mermaid"> / <pre class="mermaid"> | hand-authored, Mermaid's own docs |
| <pre><code class="language-mermaid"> | markdown-it, marked, Prism, Jekyll, Hugo, Eleventy |
| <pre class="language-mermaid"> | Shiki, Astro, Starlight, VitePress, Docusaurus |
Each one is replaced in place by a <mermaid-flow-player>, keeping its id and
your own classes. Point it somewhere else with ?selector=.
The upgrade needs the diagram source, so let the player render: don't run Mermaid with
startOnLoad: truefirst. If a block is already rendered its SVG is adopted rather than discarded, but narration falls back to node labels because the source text is gone.
Authoring new diagrams
Write the diagram inside the element and set options per diagram:
<script src="https://cdn.jsdelivr.net/npm/mermaid-flow-player/mermaid-flow-player.element.js"></script>
<mermaid-flow-player controls captions>
flowchart TD
A[Validate token] --> B[Fetch user]
%% narrate A: First we check the caller's token hasn't expired.
</mermaid-flow-player>There is one player implementation, so both routes get the same features: the
element is what the upgrade produces. Configure a block with the same
attribute names the element uses (speed, captions, speak...), with or
without a data-flow- prefix.
Install
The CDN needs no install. For a bundler:
npm install mermaid-flow-playerimport 'mermaid-flow-player/auto';CDN URL builder: docs site -> CDN Builder. Pick options and copy script tags or query params.
Usage (programmatic)
import { createFlowPlayer } from 'https://cdn.jsdelivr.net/npm/mermaid-flow-player';
// Read the source before Mermaid renders: afterwards the element holds the drawing.
const root = document.getElementById("diagram");
const source = root.textContent;
const player = createFlowPlayer({
root,
source,
visited: true,
dim: "others",
});
await player.ready();
await player.play(player.path("A", "B", "C", "E", "F", "G", "J"), { speed: 1.1 });The player reads the diagram through window.mermaid, the same Mermaid that drew it. A bundler
that imports Mermaid as a module sets it with window.mermaid = mermaid before creating a player.
Use stable, simple node IDs in your Mermaid diagram (e.g. A, B, X1) so path() and steps line up.
Animated SVG, for where JavaScript cannot run
A README, a pull request comment, an issue, an email: none of them run scripts, so none of them can
run a player. toAnimatedSvg draws a scenario as one self-contained file that animates on its own.
const svg = player.toAnimatedSvg(
[
{ type: 'node', id: 'A' },
{ type: 'edge', from: 'A', to: 'B' },
{ type: 'node', id: 'B' },
],
{ title: 'How a request is served' },
);No script, no external references, nothing to fetch. Options: stepMs (default 1200), loop
(default true), title, accent, restOpacity. A reader whose system asks for less motion gets the
finished diagram, still.
Features
Multi-Diagram Support
Auto-detects and animates 7 diagram types:
- Flowcharts, Sequence Diagrams, State Diagrams, Gantt Charts, User Journey, Class Diagrams, ER Diagrams
All diagram types use the same animation API; just change your Mermaid diagram type and the player adapts automatically.
Narration
Automatically update narration text as animation progresses:
createFlowPlayer({
root: diagram,
source,
narrationTarget: '#narration',
});
await player.play([
{ type: 'node', id: 'A', note: 'Starting...' },
{ type: 'node', id: 'B', note: 'Processing...' },
]);Narrated walkthroughs — captions, voice, transcript, deep-links
Every step (auto-play and manual Next/Previous) emits a single mfp:step
event, and the per-step text is auto-derived from the Mermaid markup — a
sequence message's label or a node's label — so authors get narration for free
just by writing normal diagrams. Sequence diagrams step through their messages
in order.
Sequence Note over/of … lines are picked up automatically as the richer
commentary for the message they follow; otherwise the message label is used.
Write a script with %% narrate. Node labels are terse ("Validate token")
but spoken narration wants a sentence. Put the script in the diagram itself and
it travels with the fenced code block — copy-paste safe, Markdown-native, and a
coding agent can emit a diagram that explains its own flow in one shot:
<mermaid-flow-player controls captions speak>
flowchart TD
A[Validate token] --> B[Fetch user]
%% narrate A: First we check the caller's token hasn't expired.
%% Expired tokens stop here.
%% narrate B: Then we load their profile from cache.
</mermaid-flow-player>A %% line indented by two or more spaces continues the previous directive, so
long narration can wrap. Scripts override the derived label everywhere at once —
captions, voice, chapters, transcript and VTT. Parse them yourself with
parseNarration(source).
On a sequence diagram, an id-less %% narrate: attaches to the message above
it — the same way Note over already does. Put %% voice <participant>: name
in the source too, so each speaker keeps their voice when the fenced block is
copied:
<mermaid-flow-player controls captions chapters speak karaoke>
sequenceDiagram
%% voice Req: daniel
%% voice W: samantha
Req->>W: POST /access-requests
%% narrate: A worker validates the caller and opens a request.
W->>WF: create instance
WF->>Req: signed approval link
</mermaid-flow-player>parseVoices(source) returns the participant → voice map. Host JavaScript
el.voices still wins when set.
Opt-in attributes:
| Attribute | Effect |
| --- | --- |
| captions | Live caption bar showing the current step's text (ARIA-announced). |
| speak | Reads each step aloud via the browser's SpeechSynthesis — zero audio files. |
| chapters | Clickable chapter rail (one entry per step); click to jump. |
| karaoke | Auto-advances when each step's narration (audio clip or voice) ends. |
| scroll-steps | Drives the step index from page scroll (scrollytelling). |
| progress | A "Step 3 of 9" position indicator. |
| step="N" / ?step=N | Deep-link straight to a step. |
Keyboard
Playback keys work on the element whether or not you render controls, and are ignored while the reader is typing in an input, textarea or contenteditable:
| Key | Action |
| --- | --- |
| Space / k | Play, pause, resume. |
| ← / → | Previous / next step. |
| Shift+←/→, ↑/↓ | Pan the viewport. |
| + / - / 0 | Zoom in / out / fit. |
| f / Escape | Open / close the focused view. |
Loading and error states
The element shows a .mfp-loading placeholder while Mermaid loads and renders,
then replaces it with the diagram. If rendering fails it shows a .mfp-error
box (role="alert") instead of leaving an empty element that looks like it is
still loading. mfp:error still fires either way; add silent-errors to
suppress the visible box and handle the event yourself.
Shape the voice (all read live — no re-render, safe to change mid-playback):
| Attribute | Effect |
| --- | --- |
| rate="0.9" | Speaking rate, 0.1–10. The default is brisk for a walkthrough; 0.9 reads better. |
| pitch="1.2" | Voice pitch, 0–2. |
| volume="0.5" | Narration volume, 0–1. |
| lang="en-GB" | BCP-47 language for the utterance. |
| voice="samantha" | Voice name, matched case-insensitively as a substring — voice names differ per platform, so a fragment beats hard-coding Samantha (Enhanced). Unmatched names keep the platform default. |
| pause-between="400" | Milliseconds of silence before karaoke advances. Without it steps advance the instant the voice stops, which reads like a queue draining rather than someone talking. |
While speaking, the caption highlights the current word (.mfp-word, styleable
via --mf-word-bg / --mf-word-color) in time with the voice.
Give each participant its own voice and a sequence diagram reads like a
conversation instead of one narrator — %% voice in the source (above), or
from JavaScript:
el.voices = (index) => (index % 2 === 0 ? 'daniel' : 'samantha');Browsers refuse speech until the page has had a user gesture, and they fail
silently. The element shows a Tap to enable voice control (.mfp-tap-to-speak)
and fires mfp:speech-blocked once, so a host can hide the default button and
prompt itself:
el.addEventListener('mfp:speech-blocked', () => showTapToStartButton());Hook mfp:step to drive your own audio commentary, analytics, or scroll-sync —
it fires on auto-play and manual stepping with { index, node, text }:
const el = document.querySelector('mermaid-flow-player');
// Option A: bind clips and let the player manage playback (+ karaoke timing)
el.audioTracks = { 0: 'audio/01.mp3', 1: 'audio/02.mp3' };
// Option B: react to the event yourself
el.addEventListener('mfp:step', (e) => {
const { index, node, text } = e.detail; // text = markup-derived narration
analytics.track('diagram_step', { index, text });
});Browser autoplay rules apply: bound audio plays after a user gesture (clicking Play/Next), so
karaokeruns once the reader interacts.
Build a transcript, captions file, or chapter list from the steps:
import { toTranscript, toVtt } from 'mermaid-flow-player';
const steps = el.player.getSteps(); // [{ index, node, text, speaker }, ...]
toTranscript(steps); // "1. ...\n2. ..." (Markdown/plain text)
toVtt(steps, { secondsPerCue: 5 }); // WebVTT subtitle trackPlayback Speed & Progress
Change speed live (even mid-run) and track progress for scrubbers/progress bars:
player.setSpeed(2); // applies immediately; emits mfp:speedchange
player.getSpeed(); // 2
player.getProgress(); // { current: 3, total: 8 } or null before first run
el.addEventListener('mfp:progress', (e) => {
const { current, total } = e.detail;
progressBar.style.width = `${(current / total) * 100}%`;
});Export
Copy or download the current diagram state — including mid-animation:
await player.copySvg(); // SVG markup to clipboard
player.downloadSvg('flow.svg');
await player.downloadPng('flow.png', { scale: 2 }); // rasterized at 2xAnimation Easing
Control animation timing with 30+ easing functions: standard CSS, back, elastic, bounce, power curves, and custom cubic-bezier().
createFlowPlayer({
root: diagram,
source,
easing: {
default: 'ease-out-back',
states: {
active: 'ease-out-elastic',
success: 'ease-out-bounce',
}
}
});Web Component
Drop-in <mermaid-flow-player> custom element. Diagram from inner text (or diagram attribute):
<script src="https://cdn.jsdelivr.net/npm/mermaid-flow-player@latest/mermaid-flow-player.element.js"></script>
<mermaid-flow-player autoplay controls>
graph LR; A-->B-->C
</mermaid-flow-player>Narration is opt-in: add the narration attribute (or narration-text="…") to show the narration area; otherwise it isn't rendered. Playback exposes a single lifecycle status — "idle" | "playing" | "paused" — via el.player.getStatus(), and every transition fires a bubbling mfp:statechange event (detail.status), which the play/pause control reflects automatically.
Interactive Mode
Step-through with user-controlled path selection:
const player = createFlowPlayer({ root: diagram, source, mode: 'interactive' });
await player.nextStep();Viewport & readable fit
Zoom, pan, and fitView() keep large diagrams legible: fit never shrinks below a readable floor, so when a diagram is too big to show whole it stays readable and the viewport scrolls/pans (pinned to the start) instead of becoming a postage stamp. Tune the floor with viewport.minReadableScale (default 0.6):
createFlowPlayer({ root: diagram, viewport: { minReadableScale: 0.6 } });Validation and user feedback
Diagram validity comes from Mermaid (they run in the browser). We propagate their errors; listen for mfp:error or use normalizeMermaidError(e) when you call mermaid.run() / mermaid.render(). For scenario-vs-index (after render), use player.validateScenario(steps) and show result.availableNodeIds for suggestions.
import { normalizeMermaidError } from '…';
try {
await mermaid.run({ nodes: [el] });
} catch (e) {
const payload = normalizeMermaidError(e);
showInUI(payload.message, payload.detail);
}Scenario-vs-index (after the diagram has rendered): use player.validateScenario(steps) to check that step IDs exist and get availableNodeIds for "Did you mean: A, B, C?". assertIds(ids) throws with available node IDs in the message.
Upgrading .mermaid blocks yourself
Auto mode runs this for you on load and re-runs it when an SPA swaps the DOM. Call it directly to control when, or to point it at a different selector:
import { autoInit } from 'https://cdn.jsdelivr.net/npm/mermaid-flow-player@latest/auto-init.js';
autoInit({ selector: '.diagram', controls: 'play-pause next fit' });controls takes true (transport plus zoom), false, a preset
("viewer", "full"), or an explicit space-separated token list. The same
values work in the element's controls attribute. destroyAll() puts the
original blocks back.
Web font
The player asks for JetBrains Mono only if you opt in — otherwise it uses the system monospace stack and makes no third-party request:
<script data-mfp-font src="…/mermaid-flow-player.element.js"></script>URL Query Parameter Configuration
Configure via URL without JavaScript: page.html?theme=dark&speed=1.5
| Parameter | Values | Default |
| --- | --- | --- |
| theme | light, dark, auto | auto |
| speed | 0.1 to 10 | 1.2 |
| stepMs | milliseconds per step | derived from speed |
| visited | true, false | true |
| mode | sequential, interactive | sequential |
| selector | CSS selector (e.g. .my-diagram) | .mermaid (auto mode only) |
| step | step index to deep-link to | none |
| autoplay | (presence) | false |
| trigger | load, scroll | load |
| debug | (presence) | false |
License
MIT
