Skip to content
rodolfo.gg
Go back

Easy graphics inclusion: Mermaid.

CC BY-NC-ND 4.0
Rodolfo González González

Easy graphics inclusion: Mermaid.

Introduction

Mermaid is a markup language that allows you to create diagrams declaratively, using a JavaScript library to generate the graphics.

Mermaid was created by Knut Sveidqvist, who was inspired by his children while watching The Little Mermaid. Initially developed as a tool to generate diagrams from text using a Markdown-like syntax, it has evolved into a full ecosystem widely used in the community: the project now has a massive user base, with over 87,500 stars on GitHub.

Mermaid integrates natively in systems such as GitHub, GitLab, Visual Studio Code, and Notion, among many others. Similarly, there are integrations for virtually all frontend libraries and frameworks.

By using a simple language like Mermaid, you can declaratively define diagrams without having to learn a visual drawing tool, which is inherently slower. It is also much easier to maintain code than a set of Gimp or El Pato source files.

I discovered it (somewhat late) while putting together the study guide for NLP.

Below are some integration examples and diagrams made with Mermaid, followed by a guide to install and customize it in Astro.


Table of Contents

Table of Contents

Depending on the stack, you can use the official mermaid library or community wrappers:


Examples

Graph TD

Source code:

```mermaid
graph TD
A[Write mermaid block in MDX] --> B[Astro processes it]
B --> C[Inline SVG is generated]
C --> D[Diagram renders in the post]
```

Rendered diagram:

graph TD
    A[Write mermaid block in MDX] --> B[Astro processes it]
    B --> C[Inline SVG is generated]
    C --> D[Diagram renders in the post]

Flowchart LR

Source code:

```mermaid
flowchart LR
Start([Start]) --> Step1[Define objective]
Step1 --> Step2[Write Mermaid block]
Step2 --> Step3[Render in Astro]
Step3 --> End([End])
```

Rendered diagram:

flowchart LR
    Start([Start]) --> Step1[Define objective]
    Step1 --> Step2[Write Mermaid block]
    Step2 --> Step3[Render in Astro]
    Step3 --> End([End])

Sequence Diagram

Source code:

```mermaid
sequenceDiagram
participant U as User
participant M as MDX
participant A as Astro
participant R as Render
U->>M: Write mermaid block
M->>A: Compile content
A->>R: Generate inline SVG
R-->>U: Show diagram
```

Rendered diagram:

sequenceDiagram
    participant U as User
    participant M as MDX
    participant A as Astro
    participant R as Render
    U->>M: Write mermaid block
    M->>A: Compile content
    A->>R: Generate inline SVG
    R-->>U: Show diagram

Mindmap

Source code:

```mermaid
mindmap
root((Fundamentals))
Language (langue)
Speech (parole)
Linguistic competence
Linguistic performance
Linguistic sign
Synchrony and diachrony
```

Rendered diagram:

mindmap
  root((Fundamentals))
    Language (langue)
    Speech (parole)
    Linguistic competence
    Linguistic performance
    Linguistic sign
    Synchrony and diachrony

Class Diagram

Source code:

```mermaid
classDiagram
class Animal {
+String name
+eat()
}
class Dog {
+bark()
}
Animal <|-- Dog
```

Rendered diagram:

classDiagram
    class Animal {
      +String name
      +eat()
    }
    class Dog {
      +bark()
    }
    Animal <|-- Dog

State Diagram

Source code:

```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Active: start()
Active --> Paused: pause()
Paused --> Active: resume()
Active --> [*]: stop()
```

Rendered diagram:

stateDiagram-v2
    [*] --> Idle
    Idle --> Active: start()
    Active --> Paused: pause()
    Paused --> Active: resume()
    Active --> [*]: stop()

Entity Relationship (ER)

Source code:

```mermaid
erDiagram
USER ||--o{ ORDER : places
ORDER ||--|{ ORDER_LINE : contains
PRODUCT ||--o{ ORDER_LINE : appears_in
```

Rendered diagram:

erDiagram
    USER ||--o{ ORDER : places
    ORDER ||--|{ ORDER_LINE : contains
    PRODUCT ||--o{ ORDER_LINE : appears_in

Gantt

Source code:

```mermaid
gantt
title Publication plan
dateFormat YYYY-MM-DD
section Content
Writing :a1, 2026-04-22, 2d
Review :a2, after a1, 1d
section Publishing
Layout :a3, after a2, 1d
Publish :milestone, m1, after a3, 0d
```

Rendered diagram:

gantt
    title Publication plan
    dateFormat  YYYY-MM-DD
    section Content
    Writing             :a1, 2026-04-22, 2d
    Review              :a2, after a1, 1d
    section Publishing
    Layout              :a3, after a2, 1d
    Publish             :milestone, m1, after a3, 0d

Pie Chart

Source code:

```mermaid
pie title Task distribution
"Documentation" : 35
"Development" : 45
"Testing" : 20
```

Rendered diagram:

pie title Task distribution
    "Documentation" : 35
    "Development" : 45
    "Testing" : 20

Git Graph

Source code:

```mermaid
gitGraph
commit id: "init"
branch feature
checkout feature
commit id: "new-diagram"
checkout main
merge feature
commit id: "release"
```

Rendered diagram:

gitGraph
    commit id: "init"
    branch feature
    checkout feature
    commit id: "new-diagram"
    checkout main
    merge feature
    commit id: "release"
See more examples here.

Installation Procedure in Astro

1. Install mermaid

Terminal window
bun add mermaid

2. Remark plugin in astro.config.ts

The problem with build-time rendering solutions (such as astro-mermaid or rehype-mermaid) is that they conflict with astro-expressive-code, the library responsible for syntax highlighting, breaking code highlighting in all ```javascript, ```bash etc. blocks.

The solution is to render Mermaid on the client side, but we need to prevent expressiveCode from processing ```mermaid blocks. A remark plugin converts those blocks to raw HTML <pre class="mermaid"> before expressiveCode sees them:

astro.config.ts
import { visit } from 'unist-util-visit';
function remarkMermaidBypass() {
return (tree: any) => {
visit(tree, 'code', (node: any, index: number | undefined, parent: any) => {
if (node.lang === 'mermaid' && parent && typeof index === 'number') {
parent.children[index] = {
type: 'html',
value: `<pre class="mermaid">\n${node.value}\n</pre>`,
};
}
});
};
}

Add it to the markdown.remarkPlugins array (order matters: it must come first):

astro.config.ts
markdown: {
remarkPlugins: [
remarkMermaidBypass, // first
remarkToc,
remarkMath,
remarkCollapse,
],
// ...
},

It is also important that the MDX integration inherits the markdown configuration rather than defining its own plugins:

astro.config.ts
mdx({
extendMarkdownConfig: true, // inherits remarkPlugins and rehypePlugins
}),

If you pass rehypePlugins or remarkPlugins directly to mdx(), they replace (not merge with) those in markdown.*, which causes rehypeExpressiveCode to silently disappear from the MDX pipeline.

3. Rendering script in the post layout

In the layout that renders posts (in Astro Paper: PostDetails.astro), add a <script> that imports mermaid and renders it on the client side. The script uses the astro:page-load event for View Transitions compatibility, and a MutationObserver to re-render when the user switches themes (light/dark):

PostDetails.astro
import mermaid from "mermaid";
let themeObserver: MutationObserver | null = null;
function getTheme() {
return document.documentElement.dataset.theme === "dark" ? "dark" : "forest";
}
async function renderMermaid() {
// Restore already-rendered diagrams to pre.mermaid for re-rendering
// (needed on theme change)
document.querySelectorAll<HTMLElement>("[data-mermaid]").forEach(el => {
const pre = document.createElement("pre");
pre.className = "mermaid";
pre.textContent = el.dataset.mermaid!;
el.replaceWith(pre);
});
const blocks = Array.from(document.querySelectorAll<HTMLPreElement>("pre.mermaid"));
if (!blocks.length) return;
mermaid.initialize({ startOnLoad: false, theme: getTheme() });
await Promise.all(blocks.map(async pre => {
const source = (pre.textContent ?? "").trim();
const id = `mermaid-${Math.random().toString(36).slice(2, 9)}`;
try {
const { svg } = await mermaid.render(id, source);
const wrapper = document.createElement("div");
wrapper.className = "my-6 flex justify-center overflow-x-auto";
wrapper.dataset.mermaid = source; // store source for re-render on theme change
wrapper.innerHTML = svg;
pre.replaceWith(wrapper);
} catch (err) {
console.error("[mermaid]", err);
}
}));
}
document.addEventListener("astro:page-load", () => {
renderMermaid();
themeObserver?.disconnect();
themeObserver = new MutationObserver(renderMermaid);
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
});
// wrong
const codeBlocks = Array.from(document.querySelectorAll("pre"));
// correct
const codeBlocks = Array.from(document.querySelectorAll("pre:not(.mermaid)"));

4. Theme detection

The site uses the data-theme attribute on <html> to indicate the active theme ("light" or "dark"). The getTheme() function reads that attribute and returns the corresponding Mermaid theme name. This blog uses "forest" for light and "dark" for dark.

Color customization with CSS

Mermaid renders SVG inline, and its internal styles use high-specificity selectors (#id .class). To override them with the site’s theme colors, !important is needed in global CSS.

There are some quirks in Mermaid v11 compared to earlier versions:

The complete CSS used in this blog, in global.css:

/* Mermaid: mindmap — non-root sections use the theme's foreground color */
svg.mindmapDiagram [class*="section-"]:not(.section-root) span {
color: var(--foreground) !important;
}
/* Mermaid: flowchart — nodes (graph TD, flowchart LR, etc.) */
[id^="mermaid-"] .node rect,
[id^="mermaid-"] .node circle,
[id^="mermaid-"] .node ellipse,
[id^="mermaid-"] .node polygon,
[id^="mermaid-"] .node path {
fill: var(--muted) !important;
stroke: var(--border) !important;
}
/* Mermaid: flowchart — edges */
[id^="mermaid-"] .edgePath .path,
[id^="mermaid-"] .flowchart-link {
stroke: var(--accent) !important;
}
/* Mermaid: flowchart — arrowheads (v11: .arrowheadPath) */
[id^="mermaid-"] .arrowheadPath {
fill: var(--accent) !important;
stroke: var(--accent) !important;
}
/* Mermaid: sequence — actors (v11: .actor directly on rect) */
[id^="mermaid-"] .actor {
fill: var(--muted) !important;
stroke: var(--border) !important;
}
/* Mermaid: sequence — message lines */
[id^="mermaid-"] .messageLine0,
[id^="mermaid-"] .messageLine1 {
stroke: var(--accent) !important;
}
/* Mermaid: sequence — arrowheads */
[id$="-arrowhead"] path {
fill: var(--accent) !important;
stroke: var(--accent) !important;
}
/* Mermaid: sequence — vertical lifelines */
[id^="mermaid-"] .actor-line {
stroke: var(--border) !important;
}

Share this post on:

Previous Post
Guide for service administration with systemd.
Next Post
How to install caveman in Visual Studio Code for Claude Code.