The ask
A tip, not a spec
- The tip was one line, so before saying yes I asked three things: does it fit Strive, what’s worth building, and what isn’t?
- It fit. Strive’s chat could already draw a Mermaid block, but only by accident: our markdown renderer happened to support it, and nothing told the model to write one. That path had rough edges, so I started with the two rules, and everything else serves them. I designed it end to end and built it pairing with Claude Code.
“Adding mermaid charts to easily explain stuff.”
three questions before a yes ↓
- Chat already drew Mermaid, by accident
- Models already know the language
- When to draw: a tool, not a prompt line
- A gate: the real parser
- A theme from our own tokens
- Expand, zoom, download, copy
- Charts with numbers: the chart tools own them
- Routine diagrams: they stay on React Flow
- Styling the model writes
- More than one per answer
Mermaid by accident
- Mermaid’s default theme, light only
- A red error box with the raw source
- Re-rendered on every streamed chunk
- No expand, download or copy
vs
Built on purposenow
- Themed from our own tokens, light and dark
- A broken diagram renders nothing
- Drawn once the source is complete
- Expand, zoom, download and copy
Design call · layers
One concern per layer
- I split it three ways: a tool that decides when, a skill that teaches how, and a validator that’s the real gate. The tool is always in the model’s context. The skill loads only before the first diagram in a conversation.
- Why not just a skill? Our router picks one best-matching skill per request, and prefers the narrower one. A diagram skill would either never load, or match every question and over-draw. So the when lives in the tool’s description, which is always there.
prompts · the model reads them
code · runs on every call
ui_mermaid_diagram
Always in context, ~350 tokens a turn. Its description is the draw or don’t-draw gate.
diagrams
Loaded before the first diagram. Which type fits, shapes for chat width, syntax pitfalls.
the real parser
A type policy and size caps. It runs on every call.
a prompt can be ignored. code can’t.
Design call · the call
A tool call, not a code fence
- The cheapest version was one prompt line: “use Mermaid code blocks when helpful”. But nothing can check a code block before it shows, and it streams in half-written.
- A tool call hands over a title, the source and a caption, which can be parsed and rejected before anyone sees them. Code blocks stay as a safety net: if the model writes one anyway, chat waits until it settles, draws it with the same component, and counts the bypass.
A Mermaid code block
- Gated by the prompt alone
- Streams in half-written
- Nothing to check before it shows
vs
A tool callpicked
- A title, the source and a caption
- Parsed before anyone sees it
- Rejected with a hint, retried once
Design call · Mermaid again
Same team, opposite answer
- In July I’d designed the routine “logic workflow” diagram, and said no to Mermaid on evidence. Plain Mermaid got about 50% of the way to the Figma mock, and getting near 90% meant HTML inside labels. So that one became a JSON graph, drawn with React Flow.
- For chat, the answer flipped. Free-form answers need sequences, journeys, mindmaps and timelines: shapes a node-and-edge graph can’t express. And Mermaid is the diagram language models already know. Both stay, each for its own job.
Routine diagrams · July
- A fixed product view, drawn to a Figma mock
- A JSON graph, rendered with React Flow
- Plain Mermaid got ~50% of the way
&
Chat answers · Septemberboth stay
- Free-form explanations of any shape
- Mermaid source, checked and themed
- Sequences, journeys, mindmaps, timelines
The gate
A broken diagram never shows
- Every call goes through the real Mermaid parser on the server. It only parses inside a browser-like window, and putting one on the server’s globals would fool other libraries into thinking they run in a browser. So it runs in its own worker thread, and fails open: if the worker is down, chat doesn’t wait on it.
- A rejected diagram goes back to the model with a targeted hint, like “quote labels with punctuation”, and one retry. Then prose. The browser draws from the tool’s arguments and parses them again, so a failed attempt just isn’t there, live or on reload.
it parses, the type is allowed, and it fits in 80 lines
Design call · the look
Meaning, not colours
- My note on the first render: don’t make them boring. Out of the box, Mermaid looks generated. So every diagram gets a clean finish from the platform’s live tokens, in light and dark and in each tenant’s brand colour. Any styling the model writes is stripped, so the theme always wins.
- Instead, the model gets a tiny vocabulary of meaning, plus a few taste rules in the skill: stadiums for start and end, decisions phrased as questions, sentence-case labels, at most one diagram per answer.
==>The one main path, bold in the brand colour-.->Optional steps, fallbacks and loop-backs:::accentThe step the answer is about:::success / warning / dangerGood, at-risk and bad outcomes:::mutedOut of scope or deprioritised
Around the diagram
Expand, download, copy
- Chat is narrow, and diagrams get big. So each one has a toolbar on hover: expand into a dialog with zoom from 50 to 300%, download a PNG at 2×, or copy the source.
- Mermaid is about 1 MB, so it loads only when a diagram mounts. A chat that never draws never pays for it.
- A title, always. Each diagram says what it shows.
- Expand, download, copy. On hover, out of the way.
- The step that matters. A brand wash, on the bold main path.
- Out of scope, muted. Negatives in quiet, dashed boxes.
- A caption with the takeaway. One line under the diagram.
Measure
Measure the gate, don’t guess
- I didn’t guess whether it would draw at the right moments. A 10-prompt eval, six that should draw and four that shouldn’t, ran through the real chat API and model.
- Rewording the tool moved nothing. The blocker was two older system-prompt lines that sent every explanation to markdown. I reworded both, shorter to fit a 12,700-token budget, and matched the skill’s description to “walk me through…” questions. Recall went from 2/6 to 3/6, and false positives stayed at 0/4.
should draw · 6
shouldn’t · 4
Baseline The first tool wording
2/6
0/4
Tool rewrite A sharper description
1–2/6
0/4
moved nothing
Prompt lines + skillshipped Two old lines reworded, the skill matched to “walk me through…”
3/6, twice
0/4
What dried
What I’d keep
-
MissReworded the tool’s description first. Recall didn’t move.
LessonMeasure prompt changes. The real blocker sat in an older line I hadn’t touched.
-
MissPlanned a fixed list of diagram types in the tool’s schema.
LessonLet code hold the policy. Allowing a type is one line, not a prompt rewrite and another eval.
-
MissGave the parser one timeout for starting and parsing. Slow cold starts were killed, respawned and killed again.
LessonSeparate budgets for separate jobs: 30 s to start, 8 s to parse. Code review caught this one.
-
MissTrusted Mermaid 11.11’s mindmap. Every node rendered black.
LessonVisual QA catches what tests can’t. A Storybook pass over every type found it, and a version bump fixed it.
Drawing a diagram is the easy half. Knowing when to draw one is the product.