Mermaid rendering paths in Astro: client-side during development, build-time SVG generation for production.
Why Mermaid Rendering Matters in a Static Astro Site
Mermaid is a powerful way to express ideas visually:
- architecture diagrams
- flowcharts
- system interactions
- mental models
In an Astro project—especially one that is fully precompiled into static pages—how Mermaid diagrams are rendered has a direct impact on:
- user experience
- perceived performance
- layout stability (flash / shift)
- long-term maintainability
At first glance, adding Mermaid support to Astro looks trivial. In practice, once you care about build-time output and dev/prod parity, the details start to matter.
This post documents my journey of rendering Mermaid diagrams at build time in Astro, and what I learned along the way.
Step 1: Starting With astro-mermaid
The most straightforward option is astro-mermaid.
Setup is simple:
- install the integration
- enable it in
astro.config - write Mermaid code blocks in Markdown
This works well, especially during development.
However, there is an important detail that is easy to miss:
astro-mermaidrenders diagrams on the client.
That means:
- Mermaid code is shipped to the browser
- SVGs are generated at runtime
- diagrams appear after the page loads
Why Client-Side Rendering Wasn’t Ideal
My Astro site is fully precompiled into static HTML.
In that context:
- there is no reason not to pre-render diagrams
- client-side Mermaid introduces a small but visible flash
- layout stability is slightly affected
- SEO and performance can be improved with static SVGs
So the goal became clear:
Precompile Mermaid diagrams into SVGs during
npm run build.
Step 2: Switching to rehype-mermaid for Build-Time Rendering
To move Mermaid rendering into the build pipeline, I switched to rehype-mermaid.
This plugin runs during Markdown processing and converts Mermaid code blocks directly into SVGs at build time.
From a rendering perspective, this was exactly what I wanted:
- no client-side JavaScript
- no runtime diagram generation
- fully static output
Theme configuration was also straightforward, and could be aligned with what I used in development.
Visually, however, something still felt off.
The Subtle Problem: Structural Differences
Even with the same Mermaid theme, development and production output did not look the same.
The reason turned out not to be styling, but structure.
astro-mermaid output (development)
<pre class="mermaid">
<svg>...</svg>
</pre>
rehype-mermaid output (build)
<svg>...</svg>
That missing wrapper is significant.
The <pre> element in development:
- naturally centers the SVG
- provides a container for background and spacing
- makes diagrams feel visually intentional
With build-time rendering, I was left with bare SVGs:
- left-aligned
- no background
- awkward spacing
At this point it became clear that this was not a theming issue, but a DOM structure issue.
Step 3: Fixing Structure Instead of Fighting CSS
Rather than trying to force SVG layout using CSS alone, I chose to fix the structure at the source.
The idea was simple:
- let
rehype-mermaidgenerate the SVG - post-process the AST
- wrap each Mermaid SVG in a container
Desired Output
<div class="mermaid">
<svg>...</svg>
</div>
Now:
- layout and presentation belong to the wrapper
- the SVG remains untouched
- development and build outputs are conceptually aligned
Step 4: Writing a Small Rehype Wrapper Plugin
I implemented a small rehype plugin that runs after rehype-mermaid.
What it does:
- walks the HAST tree
- finds
<svg>elements whoseidstarts withmermaid- - replaces them with a wrapper element
Conceptually:
visit(tree, "element", (node, index, parent) => {
if (node.tagName === "svg" && node.properties.id.startsWith("mermaid-")) {
parent.children[index] = {
type: "element",
tagName: "div",
properties: { className: ["mermaid"] },
children: [node],
};
}
});
This transformation happens entirely at build time, with no client-side cost.
Step 5: Styling the Wrapper (Not the SVG)
Once the wrapper exists, styling becomes trivial and intentional.
div.mermaid {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.02);
padding: 24px;
border-radius: 16px;
margin: 32px 0;
overflow-x: auto;
}
Key decision:
- the SVG itself is never styled
- the wrapper owns layout, spacing, and background
- Mermaid output remains pure and future-proof
Bonus: Opt-in Responsive Mermaid Diagrams (LR ↔ TB)
One extra improvement I added after this post: an opt-in responsive meta tag for Mermaid code blocks.
Usage
```mermaid responsive
flowchart LR
A --> B
The idea is simple:
- Desktop / wide screens: keep
flowchart LR(left-to-right) - Mobile / narrow screens: switch to
flowchart TB(top-to-bottom)
This keeps diagrams readable without manually maintaining two versions.
How it works
I keep it opt-in so most diagrams stay unchanged.
- A small remark plugin (
remark-mermaid-responsive) detects```mermaid responsiveblocks and injects a marker line at the top of the Mermaid source:%% mermaid-responsive %%
That marker then drives two different implementations:
Development (client-side, astro-mermaid)
Because dev uses client-side rendering, I added a tiny module script that:
- detects
pre.mermaidblocks containing the marker - replaces only the direction (
LR↔TB) based on a breakpoint - re-renders on resize / theme change / Astro page swap
This keeps the dev experience fast while still letting diagrams adapt responsively.
Production (build-time, rehype-mermaid)
For production builds, client-side re-rendering is unnecessary.
Instead, a rehype plugin (rehype-mermaid-responsive) duplicates the Mermaid code block before rehype-mermaid runs:
- one copy forced to
LR - one copy forced to
TB
It wraps them like this:
<div class="mermaid-responsive">
<div class="mermaid-responsive__item mermaid-responsive__item--lr">
<pre><code class="language-mermaid">...</code></pre>
</div>
<div class="mermaid-responsive__item mermaid-responsive__item--tb">
<pre><code class="language-mermaid">...</code></pre>
</div>
</div>
Then CSS handles which one is visible:
- show
--lrby default - switch to
--tbunder the breakpoint
That gives me responsive diagrams with zero runtime cost in production.
Takeaway
This felt like the same lesson as the wrapper fix:
- don’t fight the output with CSS alone
- instead, make the structure / pipeline produce what you actually want
With an opt-in marker, I can keep Mermaid code clean while still getting responsive behavior where it matters.
Final Setup: Development vs Production
The final setup looks like this:
Development
astro-mermaid- fast feedback loop
- client-side rendering is acceptable
Production Build
rehype-mermaid- custom wrapper plugin
- static SVG output
- no flash, better UX
This split provides a good balance between developer experience and production performance.
Final Takeaway
What surprised me most in this process was that:
- the hard part wasn’t Mermaid itself
- it wasn’t theming
- it was structure
Once the DOM structure was correct, everything else became simple.
If you are using Astro as a static-first framework, I would strongly recommend:
- rendering diagrams at build time
- fixing structure instead of compensating with CSS
- treating diagrams as first-class build artifacts
Hopefully this saves someone else a few rounds of trial and error.