What we had
Project pages and the stack writeup were full of ASCII art:
RTL.v ──► simulate ──► synthesize ──► floorplan ──► place ──► CTS ──► route ──► signoff ──► GDS ──► fab
| | | | | | | | |
you iverilog yosys openroad openroad openroad openroad magic + skywaterThis works in plain text and in Verilog comments where we actually want it (humans reading source code), but on a styled site it sticks out as unstyled monospace among the rest of the typography. It also doesn’t re-flow at narrow widths — just wraps mid-arrow.
What we wanted
- Real arrows and boxes that respect the page’s typography and the active theme.
- Live theme switching: when the daisyUI theme picker flips between the dozen+ themes we ship, the diagrams should re-skin without a re-render or a flash.
- No client-side JavaScript at runtime. Render at build time, ship the SVG, let the browser cascade CSS variables into it.
- Authoring should feel like writing a code block — not opening a separate diagram tool, exporting a file, importing it.
The pick: beautiful-mermaid
beautiful-mermaid
from Craft Labs hits all four points. It’s a re-implementation of
Mermaid’s renderer (using ELK.js for layout) that:
- Renders synchronously — the standard Mermaid renderer is async because of how it bootstraps in a browser; this one bypasses the worker so you get an SVG string straight from a function call.
- Has a mono mode that derives the entire palette from just two
colors,
bgandfg. - Accepts CSS variable references in its theme config — pass
bg: 'var(--color-base-100)'and the SVG embeds thatvar(...)reference verbatim. The browser resolves it at paint time. - Lets you supply an enrichment palette on top of mono mode
(
accent,line,border,surface,muted) and falls back tocolor-mixfrombgandfgfor anything you don’t override.
Net effect: a single rendered SVG that re-skins instantly when the theme picker changes the underlying daisyUI variables. No re-render, no JavaScript at runtime, no flash.
The component
The whole thing is one file, site/src/components/Diagram.astro. The
relevant part of the frontmatter:
const svg = renderMermaidSVG(code, {
bg: 'var(--diagram-bg, transparent)',
fg: 'var(--diagram-fg)',
accent: 'var(--diagram-accent)',
line: 'var(--diagram-line)',
muted: 'var(--diagram-muted)',
border: 'var(--diagram-border)',
surface: 'var(--diagram-surface)',
transparent: true,
});The matching CSS keys those custom properties to the daisyUI theme:
.diagram {
--diagram-fg: var(--color-base-content);
--diagram-accent: var(--color-primary);
--diagram-line: color-mix(in oklab, var(--color-base-content) 55%, transparent);
--diagram-muted: color-mix(in oklab, var(--color-base-content) 45%, transparent);
--diagram-border: color-mix(in oklab, var(--color-base-content) 30%, transparent);
--diagram-surface: color-mix(in oklab, var(--color-base-content) 6%, transparent);
}When daisyUI rotates --color-base-content and --color-primary
on the <html data-theme=...> element, every <svg> on the page
inherits the new values via the CSS cascade.
Authoring
In MDX, a diagram is a child template literal:
<Diagram caption="The harden flow" code={`
graph LR
RTL --> SIM["simulate<br/>iverilog"]
SIM --> SYN["synthesize<br/>yosys"]
SYN --> FP["floorplan<br/>OpenROAD"]
FP --> GDS
`} />Everything else — layout, styling, theme reactivity — falls out of
the component. There’s no build-step file watcher, no exported .svg
to keep in public/, no separate diagram source files. The diagram
source lives next to the prose that motivates it.
A live one
Same <Diagram> component as elsewhere on the site. Try the theme
picker in the header — the colors below shift in lockstep with the
prose around them.
Caveats and trade-offs
A few corners worth knowing about:
- The library defaults to Inter via Google Fonts in the SVG’s
embedded
<style>. We strip the@importand the matchingtext { font-family }rule with two regexes on the SVG string, then impose our mono stack via the component’s own CSS. Cuts the font network request and matches the rest of the site. - ELK.js layout runs at build time — each diagram is ~200-400ms of the build. Currently fine; if it grows we can memoize.
stateDiagram-v2works but the layout engine is happier withgraph(flowchart) syntax for control-flow drawings. We usestateDiagram-v2only when the FSM-ness of a thing is the point.- The library accepts
bg: 'var(...)'happily, buttransparent: truein the same call also injects a CSS rule to the effect. We pass both because the page background should bleed through, but the page-relative coloring should still come from the same palette.
Future hooks
If we ever want runtime Mermaid editing (e.g. an interactive
“explore the FSM” widget) we’d add a thin client-side wrapper that
calls the same renderMermaidSVG from a browser bundle, keyed on a
textarea’s contents. The library is built for this — synchronous,
zero DOM dependencies, works in the browser too — but we don’t
need it yet on a static site.