mermaid-animate

Mermaid diagrams, animated with GSAP! This is vibe coded, but it works-ish!

Flow: Edge Sequence

Mermaid

graph LR
A[Start] --> B{Branch}
B -->|yes| C[Do a thing]
B -->|no| D[Different path]
C --> E[Finish]
D --> E

mermaid-animate

<!doctype html>
<meta charset="utf-8" />
<div id="app"></div>
<script type="module">
  import animate from 'mermaid-animate';
  const el = document.getElementById('app');
  const code = `
  graph LR
  A[Start] --> B{Branch}
  B -->|yes| C[Do a thing]
  B -->|no| D[Different path]
  C --> E[Finish]
  D --> E
  `;
  animate.initialize({ theme: 'light' });
  await animate.flow.draw(el, code, { draw: { step: 1.0, gap: 0.25 }, loop: true });
</script>

Flow: Click-to-Traverse (waves)

Tip: Click any node to fan‑out traversal waves.

Mermaid

graph LR
A[Request] --> B{Router}
B -->|auth| C[Auth]
B -->|cache| D[Cache]
C --> E[Service]
D --> E
E --> F[DB]

mermaid-animate

<!doctype html>
<meta charset="utf-8" />
<style> svg{max-width:900px} </style>
<div id="app"></div>
<script type="module">
  import animate, { renderMermaid, ensurePath, animateStrokeDraw, listFlowEdgesFromSvgRaw } from 'mermaid-animate';
  import gsap from 'gsap';
  const el = document.getElementById('app');
  const code = `
  graph LR
  A[Request] --> B{Router}
  B -->|auth| C[Auth]
  B -->|cache| D[Cache]
  C --> E[Service]
  D --> E
  E --> F[DB]
  `;
  animate.initialize({ theme: 'light' });
  const { svg } = await renderMermaid(el, code);
  const raw = listFlowEdgesFromSvgRaw(svg).map(e => ({ ...e, path: ensurePath(e.path) }));
  const out = new Map(); raw.forEach(e => { if (!out.has(e.from)) out.set(e.from, []); out.get(e.from).push(e); });
  function waves(start){
    const tl = gsap.timeline({ paused:true });
    const seen=new Set([start]); let frontier=[start]; const step=0.8,gap=0.2;
    while(frontier.length){
      const next=[]; const layer=[];
      for(const u of frontier){ for(const e of (out.get(u)||[])){ layer.push(e.path); if(!seen.has(e.to)){ seen.add(e.to); next.push(e.to);} }}
      if(layer.length){ const pos = tl.duration(); layer.forEach(p => animateStrokeDraw(tl,p,{duration:step},pos)); tl.to({}, {duration:gap}); }
      frontier = next;
    }
    return tl.play(0);
  }
  // bind clicks on nodes
  svg.querySelectorAll('g.node').forEach(n => {
    n.style.cursor='pointer';
    n.addEventListener('click', () => waves(n.id.replace(/^flowchart-([^-]+)-.*/, '$1')));
  });
  // autoplay from first root and loop
  const roots = (() => { const map=new Map(); raw.forEach(e=>{map.set(e.to,true); if(!map.has(e.from)) map.set(e.from,false);}); return [...map].filter(([,v])=>!v).map(([k])=>k); })();
  const start = roots[0] || raw[0]?.from; let tl = waves(start); tl.eventCallback('onComplete', () => setTimeout(() => { tl.kill(); tl = waves(start); }, 1000));
</script>

Flow: Advanced Fan-out (auto roots)

Mermaid

graph LR
A --> B
A --> C
B --> D
C --> D
D --> E
D --> F

mermaid-animate

<!doctype html>
<meta charset="utf-8" />
<div id="app"></div>
<script type="module">
  import animate, { renderMermaid, ensurePath, animateStrokeDraw, listFlowEdgesFromSvgRaw } from 'mermaid-animate';
  import gsap from 'gsap';
  const el = document.getElementById('app');
  const code = `
  graph LR
  A --> B
  A --> C
  B --> D
  C --> D
  D --> E
  D --> F
  `;
  animate.initialize({ theme: 'light' });
  const { svg } = await renderMermaid(el, code);
  const edges = listFlowEdgesFromSvgRaw(svg).map(e => ({ ...e, path: ensurePath(e.path) }));
  const roots = (() => { const m=new Map(); edges.forEach(e=>{m.set(e.to,true); if(!m.has(e.from)) m.set(e.from,false);}); return [...m].filter(([,v])=>!v).map(([k])=>k); })();
  const out = new Map(); edges.forEach(e => { if (!out.has(e.from)) out.set(e.from, []); out.get(e.from).push(e); });
  const master = gsap.timeline({ paused:true, repeat:-1, repeatDelay:1 });
  const step = 0.7, gap = 0.2;
  roots.forEach(r => {
    const child = gsap.timeline();
    let frontier=[r]; const seen=new Set([r]);
    while(frontier.length){ const next=[]; const wave=[]; for(const u of frontier){ for(const e of (out.get(u)||[])){ wave.push(e.path); if(!seen.has(e.to)){ seen.add(e.to); next.push(e.to);} }} const pos=child.duration(); wave.forEach(p=>animateStrokeDraw(child,p,{duration:step},pos)); child.to({}, {duration:gap}); frontier=next; }
    master.add(child, 0);
  });
  master.play(0);
</script>

Flow: Splitting Tracers (continuous)

Mermaid

graph LR
A --> B
A --> C
B --> D
C --> E
D --> F
E --> F

mermaid-animate

<!doctype html>
<meta charset="utf-8" />
<div id="app"></div>
<script type="module">
  import animate, { renderMermaid, ensurePath, createTracerDot, listFlowEdgesFromSvgRaw } from 'mermaid-animate';
  import { MotionPathPlugin } from 'gsap/all';
  import gsap from 'gsap';
  gsap.registerPlugin(MotionPathPlugin);
  const el = document.getElementById('app');
  const code = `
  graph LR
  A --> B
  A --> C
  B --> D
  C --> E
  D --> F
  E --> F
  `;
  animate.initialize({ theme: 'light' });
  const { svg } = await renderMermaid(el, code);
  const edges = listFlowEdgesFromSvgRaw(svg).map(e => ({ ...e, path: ensurePath(e.path) }));
  const out = new Map(); edges.forEach(e => { if (!out.has(e.from)) out.set(e.from, []); out.get(e.from).push(e); });
  const roots = (() => { const m=new Map(); edges.forEach(e=>{m.set(e.to,true); if(!m.has(e.from)) m.set(e.from,false);}); return [...m].filter(([,v])=>!v).map(([k])=>k); })();
  const parent = svg.querySelector('g.root') || svg;
  let token = 0; let timer = null;
  function advance(dot, node, t){
    const es = out.get(node) || [];
    if (es.length === 0) { dot.remove(); return; }
    const step = 0.8;
    es.forEach((e,i) => {
      const d = i === 0 ? dot : dot.cloneNode(true);
      if (i > 0) parent.appendChild(d);
      try { gsap.killTweensOf(d); } catch {}
      d.removeAttribute('transform'); d.removeAttribute('data-svg-origin'); gsap.set(d, { x:0, y:0, rotation:0 });
      d.setAttribute('cx','0'); d.setAttribute('cy','0'); d.setAttribute('visibility','visible');
      gsap.to(d, { duration: step, ease: 'power1.inOut', motionPath: { path: e.path, autoRotate:false }, overwrite:'auto', immediateRender:false,
        onComplete: () => { if (t!==token) return; advance(d, e.to, t); } });
    });
  }
  function run(){
    token++; if (timer) { clearTimeout(timer); timer = null; }
    svg.querySelectorAll('circle[data-follower="1"]').forEach(n => n.remove());
    roots.forEach(r => advance(createTracerDot(svg, 4, '#e85d04', parent), r, token));
    timer = setTimeout(() => { if (token===token) run(); }, 2000);
  }
  run();
</script>

Flow: Heatmap (pulse)

Mermaid

graph LR
A --> B
A --> C
B --> D
C --> D
D --> E

mermaid-animate

<!doctype html>
<meta charset="utf-8" />
<div id="app"></div>
<script type="module">
  import animate, { renderMermaid } from 'mermaid-animate';
  import gsap from 'gsap';
  const el = document.getElementById('app');
  const code = `
  graph LR
  A --> B
  A --> C
  B --> D
  C --> D
  D --> E
  `;
  animate.initialize({ theme: 'light' });
  const { svg } = await renderMermaid(el, code);
  Array.from(svg.querySelectorAll('g.edgePaths path')).forEach((p,i) => {
    const hue = (i*47)%360; p.style.stroke = `hsl(${hue},80%,50%)`;
    gsap.to(p, { strokeWidth: 3, duration: 0.6, yoyo: true, repeat: -1, ease: 'sine.inOut' });
  });
</script>

Sequence: Tracer loop

Mermaid

sequenceDiagram
participant A as Client
participant B as API
participant C as DB
A->>B: Request
B->>C: Query
C-->>B: Rows
B-->>A: Response

mermaid-animate

<!doctype html>
<meta charset="utf-8" />
<div id="app"></div>
<button id="more">Add another tracer!</button>
<script type="module">
  import animate from 'mermaid-animate';
  import { MotionPathPlugin } from 'gsap/all';
  import gsap from 'gsap';
  gsap.registerPlugin(MotionPathPlugin);
  const el = document.getElementById('app');
  const code = `
  sequenceDiagram
  participant A as Client
  participant B as API
  participant C as DB
  A->>B: Request
  B->>C: Query
  C-->>B: Rows
  B-->>A: Response
  `;
  animate.initialize({ theme: 'light' });
  await animate.sequence.tracer(el, code, { tracer: { count: 1 } });
  document.getElementById('more').addEventListener('click', async () => {
    const svg = el.querySelector('svg'); if (!svg) return;
    const sels = ['path[marker-end]','line[marker-end]','polyline[marker-end]','path[marker-start]','line[marker-start]','polyline[marker-start]'];
    const paths = Array.from(svg.querySelectorAll(sels.join(','))).map(el => MotionPathPlugin.convertToPath(el, true)).map(r => Array.isArray(r)? r[0] : r);
    const dot = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg','circle'); dot.setAttribute('r','4'); dot.setAttribute('fill','hotpink'); dot.setAttribute('data-follower','1'); dot.setAttribute('visibility','hidden');
    (svg.querySelector('g.root')||svg).appendChild(dot);
    const tl = gsap.timeline({ paused:true }); paths.forEach(p => tl.to(dot, { duration: 1.0, ease: 'power1.inOut', motionPath: { path: p, align: p, autoRotate: false }, onStart: () => dot.setAttribute('visibility','visible') })); tl.eventCallback('onComplete', () => tl.play(0)); tl.play(0);
  });
</script>

State: Step‑Through Tracer

Tip: Click a state to choose the starting transition.

Mermaid

stateDiagram-v2
[*] --> Idle
Idle --> Working: Start
Working --> Idle: Stop
Working --> Error: Fail
Error --> Idle: Reset

mermaid-animate

<!doctype html>
<meta charset="utf-8" />
<div id="app"></div>
<script type="module">
  import animate, { renderMermaid, ensurePath, createTracerDot, parseTransitions } from 'mermaid-animate';
  import gsap from 'gsap';
  import { MotionPathPlugin } from 'gsap/all';
  gsap.registerPlugin(MotionPathPlugin);
  const el = document.getElementById('app');
  const code = `
  stateDiagram-v2
  [*] --> Idle
  Idle --> Working: Start
  Working --> Idle: Stop
  Working --> Error: Fail
  Error --> Idle: Reset
  `;
  animate.initialize({ theme: 'light' });
  const { svg } = await renderMermaid(el, code);
  const transitions = parseTransitions(code);
  const sels = ['path[marker-end]','line[marker-end]','polyline[marker-end]','path[marker-start]','line[marker-start]','polyline[marker-start]'];
  const connectors = Array.from(svg.querySelectorAll(sels.join(','))).map(el => ensurePath(el));
  let paths = connectors.slice();
  if (paths.length === transitions.length + 1) paths = paths.slice(1);
  else if (paths.length > transitions.length) paths = paths.slice(0, transitions.length);
  const nodes = Array.from(svg.querySelectorAll('g.node'));
  const parent = svg.querySelector('g.root') || svg;
  let token = 0; let current = null;
  function rotate(arr, k){ const n = arr.length; if (!n) return arr.slice(); const i=((k%n)+n)%n; return arr.slice(i).concat(arr.slice(0,i)); }
  function run(start){ token++; if (current){ try{ current.kill(); }catch{} current=null; } svg.querySelectorAll('circle[data-follower="1"]').forEach(n=>n.remove()); const dot=createTracerDot(svg,4,getComputedStyle(svg).getPropertyValue('--ma-accent')||'#1b74e4', parent); const tl=gsap.timeline({paused:true}); current=tl; rotate(paths,start).forEach(p=> tl.to(dot,{duration:0.9,ease:'power1.inOut',motionPath:{path:p,align:p,autoRotate:false}, onStart:()=>dot.setAttribute('visibility','visible')})); tl.eventCallback('onComplete',() => setTimeout(() => run(start), 1000)); tl.play(0); }
  run(0);
  nodes.forEach(n => { n.style.cursor='pointer'; n.addEventListener('click', () => { const label=(n.textContent||'').trim(); const idx=transitions.findIndex(t=>t.from===label); if (idx>=0) run(idx); }); });
</script>

Mapping heuristic: we match transitions to visible connectors by order; if there’s one extra connector (usually [*] → first state), we skip the first.

Playground