ADR-0003: Render Mermaid diagrams client-side instead of at build time
Read the transcript
1. The clash between static rendering and a live toggle
Host: Welcome back — today we’re digging into ADR-0003, which is all about rendering Mermaid diagrams on a site that also has to support dark mode. The tricky part is that Mermaid only knows how to render one theme at a time, but our readers can flip that dark mode toggle whenever they feel like it, no page reload required.
Guest: Right, and that mismatch is really the whole story here. If you bake the diagrams into SVG at build time, you get a fast page with zero rendering JavaScript shipped to the browser, but you’re locking in one theme forever. So the question the team had to answer was blunt: do we pre-render for speed, or render client-side so the diagrams can actually react live to that toggle?
2. Weighing the options, landing on mermaid.render()
Host: So walk me through the actual options on the table. Where did the team start looking?
Guest: First was build-time rendering with rehype-mermaid, which is great because it ships zero client JS and even works without JavaScript enabled. But rehype-mermaid drives a headless Chromium during astro build — so now every contributor and every CI run needs that dependency. Then there’s client-side rendering with mermaid dot run, Mermaid’s own DOM-scanning API, which is simple to wire up but replaces the diagram element’s content in place — once it consumes the source text to draw the diagram, that text is gone, so re-rendering later for a theme change has nothing to work from.
Host: So neither of those actually solves the re-render problem. What was the third option?
Guest: mermaid dot render, called with an id and the source text, which lets you keep the original source in a data- attribute on the container so it survives the render instead of being consumed. Pair that with a MutationObserver watching html’s data-theme attribute — the same attribute Starlight’s toggle already sets — and a theme flip just triggers a re-render from that stored source. That’s what got implemented in Mermaid.astro: render once on load, then re-render live on any theme change, no duplicate build artifacts.
3. Living with the trade-offs
Host: So what’s the actual cost of living with this? I imagine there’s a flash before the diagram renders, and presumably it just doesn’t work without JavaScript at all.
Guest: Right on both counts — you get a brief flash of the raw pre fallback before Mermaid hydrates, and no JS means no diagram, full stop, so we added role=img plus that visible fallback text to keep it accessible rather than just broken. In exchange the build pipeline stays fast with no headless browser to flake in CI, and there’s one sharp gotcha we documented right in the component: diagram source has to come in as a code prop from a .mmd file, not slot content, because MDX mangles whitespace-sensitive syntax. If analytics ever show a real chunk of no-JS readers, that’s the trigger to revisit toward build-time SVG pairs, but for now this is accepted and shipped in Mermaid.astro.
Generated from this page by Claude Sonnet 5 on , spoken by Kokoro-82M running locally. Two synthetic voices, not a recorded conversation. Every claim is drawn from this page — where it differs from the text above, the text is correct.
Status
Section titled “Status”Accepted. Implemented in packages/diagrams/src/Mermaid.astro.
Context
Section titled “Context”The platform requires Mermaid diagram support (flowcharts, sequence diagrams, architecture diagrams, state diagrams) across a site that also requires dark mode. Mermaid needs to know which theme to render with, and a reader can toggle light/dark at any time without a page reload.
Problem
Section titled “Problem”Should diagrams be rendered to SVG at build time (so the page ships zero diagram-rendering JS), or rendered in the browser (so the same markup can react to a live theme toggle)?
Options
Section titled “Options”- Build-time rendering via
rehype-mermaid. Produces static SVG with no client-side JS and works without JavaScript enabled — the better default for a static docs site in general. But Mermaid has one theme per render, so reacting to a live light/dark toggle means pre-rendering two SVGs per diagram and swapping between them with CSS, andrehype-mermaiddrives a headless browser duringastro build, which adds a Chromium dependency to every CI build and local build for every contributor. - Client-side rendering via
mermaid.run(), Mermaid’s own DOM-scanning API. Simple to wire up, but it replaces a diagram element’s content in place, which makes it awkward to re-render the same diagram from its original source when the theme changes — the source text is gone once Mermaid consumes it. - Client-side rendering via
mermaid.render(id, source), keeping the original diagram source in adata-attribute on the container so it survives being re-rendered, with aMutationObserveron<html data-theme>(the attribute Starlight’s theme toggle already sets) triggering a re-render.
Decision
Section titled “Decision”Render Mermaid diagrams client-side using mermaid.render(), implemented in
packages/diagrams/src/Mermaid.astro. The component keeps the diagram’s source in a data-
attribute, renders once on load, and re-renders whenever <html>’s data-theme attribute changes
— so a diagram matches the reader’s current theme immediately, including a toggle mid-session, with
no duplicate build artifacts.
Consequences
Section titled “Consequences”- Diagrams show a brief unstyled flash (the
<pre>fallback) before Mermaid hydrates, and require JavaScript to render at all. Arole="img"label and the visible fallback text keep the content accessible to screen readers and no-JS clients even though the rendered graphic isn’t available to them. - No headless-browser dependency in the build pipeline —
astro buildstays fast and has one fewer thing that can flake in CI. - Diagram source must be passed as a
codeprop (typically imported from a.mmdfile via?raw), not written inside the component’s default slot — MDX rewrites plain-text slot content (line breaks become<br/>, etc.), which corrupts Mermaid’s whitespace-sensitive syntax. This is documented directly in the component’s doc comment so it isn’t rediscovered by trial and error. - If the unstyled-flash trade-off stops being acceptable — for example, if analytics show a meaningful number of readers with JavaScript disabled — revisit this decision in favor of build-time rendering with a light/dark SVG pair per diagram.
References
Section titled “References”- Mermaid’s JavaScript API
rehype-mermaid, the build-time alternative considered and rejected above.- Starlight’s
data-themeattribute on<html>, which every built-in Starlight component already reads for the same light/dark switch this component observes.