Repository: lukilabs/beautiful-mermaid
Branch: main
Commit: 65f4e0ab8c28
Files: 196
Total size: 1.5 MB
Directory structure:
gitextract_riz8wh68/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── bench.ts
├── dev.ts
├── index.ts
├── package.json
├── samples-data.ts
├── src/
│ ├── __tests__/
│ │ ├── ascii-edge-styles.test.ts
│ │ ├── ascii-multiline.test.ts
│ │ ├── ascii.test.ts
│ │ ├── class-arrow-directions.test.ts
│ │ ├── class-integration.test.ts
│ │ ├── class-parser.test.ts
│ │ ├── edge-approach-direction.test.ts
│ │ ├── er-integration.test.ts
│ │ ├── er-parser.test.ts
│ │ ├── integration.test.ts
│ │ ├── layout-disconnected.test.ts
│ │ ├── linkstyle.test.ts
│ │ ├── multiline-labels.test.ts
│ │ ├── parser.test.ts
│ │ ├── renderer.test.ts
│ │ ├── sequence-integration.test.ts
│ │ ├── sequence-layout.test.ts
│ │ ├── sequence-parser.test.ts
│ │ ├── styles.test.ts
│ │ ├── testdata/
│ │ │ ├── ascii/
│ │ │ │ ├── ampersand_lhs.txt
│ │ │ │ ├── ampersand_lhs_and_rhs.txt
│ │ │ │ ├── ampersand_rhs.txt
│ │ │ │ ├── ampersand_td_fanin.txt
│ │ │ │ ├── ampersand_td_fanout.txt
│ │ │ │ ├── ampersand_without_edge.txt
│ │ │ │ ├── back_reference_from_child.txt
│ │ │ │ ├── backlink_from_bottom.txt
│ │ │ │ ├── backlink_from_top.txt
│ │ │ │ ├── backlink_with_short_y_padding.txt
│ │ │ │ ├── cls_all_relationships.txt
│ │ │ │ ├── cls_annotation.txt
│ │ │ │ ├── cls_association.txt
│ │ │ │ ├── cls_basic.txt
│ │ │ │ ├── cls_dependency.txt
│ │ │ │ ├── cls_inheritance.txt
│ │ │ │ ├── cls_methods.txt
│ │ │ │ ├── comments.txt
│ │ │ │ ├── custom_padding.txt
│ │ │ │ ├── duplicate_labels.txt
│ │ │ │ ├── er_attributes.txt
│ │ │ │ ├── er_basic.txt
│ │ │ │ ├── er_identifying.txt
│ │ │ │ ├── flowchart_tb_simple.txt
│ │ │ │ ├── graph_bt_direction.txt
│ │ │ │ ├── graph_tb_direction.txt
│ │ │ │ ├── nested_subgraphs_with_labels.txt
│ │ │ │ ├── preserve_order_of_definition.txt
│ │ │ │ ├── self_reference.txt
│ │ │ │ ├── self_reference_with_edge.txt
│ │ │ │ ├── seq_basic.txt
│ │ │ │ ├── seq_multiple_messages.txt
│ │ │ │ ├── seq_self_message.txt
│ │ │ │ ├── single_node.txt
│ │ │ │ ├── single_node_longer_name.txt
│ │ │ │ ├── subgraph_complex_mixed.txt
│ │ │ │ ├── subgraph_complex_nested.txt
│ │ │ │ ├── subgraph_direction_override.txt
│ │ │ │ ├── subgraph_empty.txt
│ │ │ │ ├── subgraph_mixed_nodes.txt
│ │ │ │ ├── subgraph_mixed_nodes_td.txt
│ │ │ │ ├── subgraph_multiple_edges.txt
│ │ │ │ ├── subgraph_multiple_nodes.txt
│ │ │ │ ├── subgraph_nested.txt
│ │ │ │ ├── subgraph_nested_with_external.txt
│ │ │ │ ├── subgraph_node_outside_lr.txt
│ │ │ │ ├── subgraph_single_node.txt
│ │ │ │ ├── subgraph_td_direction.txt
│ │ │ │ ├── subgraph_td_multiple.txt
│ │ │ │ ├── subgraph_td_multiple_paddingy.txt
│ │ │ │ ├── subgraph_three_levels_nested.txt
│ │ │ │ ├── subgraph_three_separate.txt
│ │ │ │ ├── subgraph_two_separate.txt
│ │ │ │ ├── subgraph_with_labels.txt
│ │ │ │ ├── three_nodes.txt
│ │ │ │ ├── three_nodes_single_line.txt
│ │ │ │ ├── two_layer_single_graph.txt
│ │ │ │ ├── two_layer_single_graph_longer_names.txt
│ │ │ │ ├── two_nodes_linked.txt
│ │ │ │ ├── two_nodes_longer_names.txt
│ │ │ │ ├── two_root_nodes.txt
│ │ │ │ ├── two_root_nodes_longer_names.txt
│ │ │ │ └── two_single_root_nodes.txt
│ │ │ └── unicode/
│ │ │ ├── ampersand_lhs.txt
│ │ │ ├── ampersand_lhs_and_rhs.txt
│ │ │ ├── ampersand_rhs.txt
│ │ │ ├── ampersand_without_edge.txt
│ │ │ ├── back_reference_from_child.txt
│ │ │ ├── backlink_from_bottom.txt
│ │ │ ├── backlink_from_top.txt
│ │ │ ├── cls_all_relationships.txt
│ │ │ ├── cls_annotation.txt
│ │ │ ├── cls_association.txt
│ │ │ ├── cls_basic.txt
│ │ │ ├── cls_dependency.txt
│ │ │ ├── cls_inheritance.txt
│ │ │ ├── cls_methods.txt
│ │ │ ├── comments.txt
│ │ │ ├── duplicate_labels.txt
│ │ │ ├── er_attributes.txt
│ │ │ ├── er_basic.txt
│ │ │ ├── er_identifying.txt
│ │ │ ├── graph_bt_direction.txt
│ │ │ ├── preserve_order_of_definition.txt
│ │ │ ├── self_reference.txt
│ │ │ ├── self_reference_with_edge.txt
│ │ │ ├── seq_basic.txt
│ │ │ ├── seq_multiple_messages.txt
│ │ │ ├── seq_self_message.txt
│ │ │ ├── single_node.txt
│ │ │ ├── single_node_longer_name.txt
│ │ │ ├── three_nodes.txt
│ │ │ ├── three_nodes_single_line.txt
│ │ │ ├── two_layer_single_graph.txt
│ │ │ ├── two_layer_single_graph_longer_names.txt
│ │ │ ├── two_nodes_linked.txt
│ │ │ ├── two_nodes_longer_names.txt
│ │ │ ├── two_root_nodes.txt
│ │ │ ├── two_root_nodes_longer_names.txt
│ │ │ └── two_single_root_nodes.txt
│ │ ├── text-metrics.test.ts
│ │ ├── xychart-ascii.test.ts
│ │ └── xychart-integration.test.ts
│ ├── ascii/
│ │ ├── ansi.ts
│ │ ├── canvas.ts
│ │ ├── class-diagram.ts
│ │ ├── converter.ts
│ │ ├── draw.ts
│ │ ├── edge-bundling.ts
│ │ ├── edge-routing.ts
│ │ ├── er-diagram.ts
│ │ ├── grid.ts
│ │ ├── index.ts
│ │ ├── multiline-utils.ts
│ │ ├── pathfinder.ts
│ │ ├── sequence.ts
│ │ ├── shapes/
│ │ │ ├── circle.ts
│ │ │ ├── corners.ts
│ │ │ ├── diamond.ts
│ │ │ ├── hexagon.ts
│ │ │ ├── index.ts
│ │ │ ├── rectangle.ts
│ │ │ ├── rounded.ts
│ │ │ ├── special.ts
│ │ │ ├── stadium.ts
│ │ │ ├── state.ts
│ │ │ └── types.ts
│ │ ├── types.ts
│ │ ├── validate.ts
│ │ └── xychart.ts
│ ├── browser.ts
│ ├── class/
│ │ ├── layout.ts
│ │ ├── parser.ts
│ │ ├── renderer.ts
│ │ └── types.ts
│ ├── elk-instance.ts
│ ├── er/
│ │ ├── layout.ts
│ │ ├── parser.ts
│ │ ├── renderer.ts
│ │ └── types.ts
│ ├── index.ts
│ ├── layout-engine.ts
│ ├── layout.ts
│ ├── multiline-utils.ts
│ ├── parser.ts
│ ├── renderer.ts
│ ├── sequence/
│ │ ├── layout.ts
│ │ ├── parser.ts
│ │ ├── renderer.ts
│ │ └── types.ts
│ ├── shape-clipping.ts
│ ├── styles.ts
│ ├── text-metrics.ts
│ ├── theme.ts
│ ├── types.ts
│ └── xychart/
│ ├── colors.ts
│ ├── layout.ts
│ ├── parser.ts
│ ├── renderer.ts
│ └── types.ts
├── tsconfig.json
├── tsup.config.ts
├── wrangler.toml
├── xychart-design.md
├── xychart-samples-data.ts
├── xychart-test.html
└── xychart-test.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run tests
run: bun test
- name: Type check
run: bun x tsc --noEmit
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish to npm
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Setup Node.js (for npm publish)
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: bun install
- name: Run tests
run: bun test
- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
# Dependencies
node_modules/
# Build output
dist/
site/
*.tsbuildinfo
# Generated files
index.html
# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Logs
*.log
npm-debug.log*
# Environment
.env
.env.local
.env.*.local
# Test coverage
coverage/
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2026 Craft Docs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# beautiful-mermaid
**Render Mermaid diagrams as beautiful SVGs or ASCII art**
Ultra-fast, fully themeable, zero DOM dependencies. Built for the AI era.

[](https://www.npmjs.com/package/beautiful-mermaid)
[](LICENSE)
[**Live Demo & Samples**](https://agents.craft.do/mermaid)
**[→ Use it live in Craft Agents](https://agents.craft.do)**
---
## Why We Built This
Diagrams are essential for AI-assisted programming. When you're working with an AI coding assistant, being able to visualize data flows, state machines, and system architecture—directly in your terminal or chat interface—makes complex concepts instantly graspable.
[Mermaid](https://mermaid.js.org/) is the de facto standard for text-based diagrams. It's brilliant. But the default renderer has problems:
- **Aesthetics** — Might be personal preference, but wished they looked more professional
- **Complex theming** — Customizing colors requires wrestling with CSS classes
- **No terminal output** — Can't render to ASCII for CLI tools
- **Heavy dependencies** — Pulls in a lot of code for simple diagrams
We built `beautiful-mermaid` at [Craft](https://craft.do) to power diagrams in [Craft Agents](https://agents.craft.do). It's fast, beautiful, and works everywhere—from rich UIs to plain terminals.
The ASCII rendering engine is based on [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii) by Alexander Grooff. We ported it from Go to TypeScript and extended it. Thank you Alexander for the excellent foundation! (And inspiration that this was possible.)
## Features
- **6 diagram types** — Flowcharts, State, Sequence, Class, ER, and XY Charts (bar, line, combined)
- **Dual output** — SVG for rich UIs, ASCII/Unicode for terminals
- **Synchronous rendering** — No async, no flash. Works with React `useMemo()`
- **15 built-in themes** — And dead simple to add your own
- **Full Shiki compatibility** — Use any VS Code theme directly
- **Live theme switching** — CSS custom properties, no re-render needed
- **Mono mode** — Beautiful diagrams from just 2 colors
- **Zero DOM dependencies** — Pure TypeScript, works everywhere
- **Ultra-fast** — Renders 100+ diagrams in under 500ms
## Installation
```bash
npm install beautiful-mermaid
# or
bun add beautiful-mermaid
# or
pnpm add beautiful-mermaid
```
## Quick Start
### SVG Output
```typescript
import { renderMermaidSVG } from 'beautiful-mermaid'
const svg = renderMermaidSVG(`
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Action]
B -->|No| D[End]
`)
```
Rendering is **fully synchronous** — no `await`, no promises. The ELK.js layout engine runs synchronously via a FakeWorker bypass, so you get your SVG string instantly.
Need async? Use `renderMermaidSVGAsync()` — same output, returns a `Promise`.
### ASCII Output
```typescript
import { renderMermaidASCII } from 'beautiful-mermaid'
const ascii = renderMermaidASCII(`graph LR; A --> B --> C`)
```
```
┌───┐ ┌───┐ ┌───┐
│ │ │ │ │ │
│ A │────►│ B │────►│ C │
│ │ │ │ │ │
└───┘ └───┘ └───┘
```
---
## React Integration
Because rendering is synchronous, you can use `useMemo()` for zero-flash diagram rendering:
```tsx
import { renderMermaidSVG } from 'beautiful-mermaid'
function MermaidDiagram({ code }: { code: string }) {
const { svg, error } = React.useMemo(() => {
try {
return {
svg: renderMermaidSVG(code, {
bg: 'var(--background)',
fg: 'var(--foreground)',
transparent: true,
}),
error: null,
}
} catch (err) {
return { svg: null, error: err instanceof Error ? err : new Error(String(err)) }
}
}, [code])
if (error) return {error.message}
return
}
```
**Why this works well:**
- **No flash** — SVG is computed synchronously during render, not in a useEffect
- **CSS variables** — Pass `var(--background)` etc. instead of hex colors. The SVG inherits from your app's CSS, so theme switches apply instantly without re-rendering
- **Memoized** — Only re-renders when `code` changes
---
## Theming
The theming system is the heart of `beautiful-mermaid`. It's designed to be both powerful and dead simple.
### The Two-Color Foundation
Every diagram needs just two colors: **background** (`bg`) and **foreground** (`fg`). That's it. From these two colors, the entire diagram is derived using `color-mix()`:
```typescript
const svg = renderMermaidSVG(diagram, {
bg: '#1a1b26', // Background
fg: '#a9b1d6', // Foreground
})
```
This is **Mono Mode**—a coherent, beautiful diagram from just two colors. The system automatically derives:
| Element | Derivation |
|---------|------------|
| Text | `--fg` at 100% |
| Secondary text | `--fg` at 60% into `--bg` |
| Edge labels | `--fg` at 40% into `--bg` |
| Faint text | `--fg` at 25% into `--bg` |
| Connectors | `--fg` at 50% into `--bg` |
| Arrow heads | `--fg` at 85% into `--bg` |
| Node fill | `--fg` at 3% into `--bg` |
| Group header | `--fg` at 5% into `--bg` |
| Inner strokes | `--fg` at 12% into `--bg` |
| Node stroke | `--fg` at 20% into `--bg` |
### Enriched Mode
For richer themes, you can provide optional "enrichment" colors that override specific derivations:
```typescript
const svg = renderMermaidSVG(diagram, {
bg: '#1a1b26',
fg: '#a9b1d6',
// Optional enrichment:
line: '#3d59a1', // Edge/connector color
accent: '#7aa2f7', // Arrow heads, highlights
muted: '#565f89', // Secondary text, labels
surface: '#292e42', // Node fill tint
border: '#3d59a1', // Node stroke
})
```
If an enrichment color isn't provided, it falls back to the `color-mix()` derivation. This means you can provide just the colors you care about.
### CSS Custom Properties = Live Switching
All colors are CSS custom properties on the `` element. This means you can switch themes instantly without re-rendering:
```javascript
// Switch theme by updating CSS variables
svg.style.setProperty('--bg', '#282a36')
svg.style.setProperty('--fg', '#f8f8f2')
// The entire diagram updates immediately
```
For React apps, pass CSS variable references instead of hex values:
```typescript
const svg = renderMermaidSVG(diagram, {
bg: 'var(--background)',
fg: 'var(--foreground)',
accent: 'var(--accent)',
transparent: true,
})
// Theme switches apply automatically via CSS cascade — no re-render needed
```
### Built-in Themes
15 carefully curated themes ship out of the box:
| Theme | Type | Background | Accent |
|-------|------|------------|--------|
| `zinc-light` | Light | `#FFFFFF` | Derived |
| `zinc-dark` | Dark | `#18181B` | Derived |
| `tokyo-night` | Dark | `#1a1b26` | `#7aa2f7` |
| `tokyo-night-storm` | Dark | `#24283b` | `#7aa2f7` |
| `tokyo-night-light` | Light | `#d5d6db` | `#34548a` |
| `catppuccin-mocha` | Dark | `#1e1e2e` | `#cba6f7` |
| `catppuccin-latte` | Light | `#eff1f5` | `#8839ef` |
| `nord` | Dark | `#2e3440` | `#88c0d0` |
| `nord-light` | Light | `#eceff4` | `#5e81ac` |
| `dracula` | Dark | `#282a36` | `#bd93f9` |
| `github-light` | Light | `#ffffff` | `#0969da` |
| `github-dark` | Dark | `#0d1117` | `#4493f8` |
| `solarized-light` | Light | `#fdf6e3` | `#268bd2` |
| `solarized-dark` | Dark | `#002b36` | `#268bd2` |
| `one-dark` | Dark | `#282c34` | `#c678dd` |
```typescript
import { renderMermaidSVG, THEMES } from 'beautiful-mermaid'
const svg = renderMermaidSVG(diagram, THEMES['tokyo-night'])
```
### Adding Your Own Theme
Creating a theme is trivial. At minimum, just provide `bg` and `fg`:
```typescript
const myTheme = {
bg: '#0f0f0f',
fg: '#e0e0e0',
}
const svg = renderMermaidSVG(diagram, myTheme)
```
Want richer colors? Add any of the optional enrichments:
```typescript
const myRichTheme = {
bg: '#0f0f0f',
fg: '#e0e0e0',
accent: '#ff6b6b', // Pop of color for arrows
muted: '#666666', // Subdued labels
}
```
### Full Shiki Compatibility
Use **any VS Code theme** directly via Shiki integration. This gives you access to hundreds of community themes:
```typescript
import { getSingletonHighlighter } from 'shiki'
import { renderMermaidSVG, fromShikiTheme } from 'beautiful-mermaid'
// Load any theme from Shiki's registry
const highlighter = await getSingletonHighlighter({
themes: ['vitesse-dark', 'rose-pine', 'material-theme-darker']
})
// Extract diagram colors from the theme
const colors = fromShikiTheme(highlighter.getTheme('vitesse-dark'))
const svg = renderMermaidSVG(diagram, colors)
```
The `fromShikiTheme()` function intelligently maps VS Code editor colors to diagram roles:
| Editor Color | Diagram Role |
|--------------|--------------|
| `editor.background` | `bg` |
| `editor.foreground` | `fg` |
| `editorLineNumber.foreground` | `line` |
| `focusBorder` / keyword token | `accent` |
| comment token | `muted` |
| `editor.selectionBackground` | `surface` |
| `editorWidget.border` | `border` |
---
## Supported Diagrams
### Flowcharts
```
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Process]
B -->|No| D[End]
C --> D
```
All directions supported: `TD` (top-down), `LR` (left-right), `BT` (bottom-top), `RL` (right-left).
### State Diagrams
```
stateDiagram-v2
[*] --> Idle
Idle --> Processing: start
Processing --> Complete: done
Complete --> [*]
```
### Sequence Diagrams
```
sequenceDiagram
Alice->>Bob: Hello Bob!
Bob-->>Alice: Hi Alice!
Alice->>Bob: How are you?
Bob-->>Alice: Great, thanks!
```
### Class Diagrams
```
classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal: +int age
Animal: +String gender
Animal: +isMammal() bool
Duck: +String beakColor
Duck: +swim()
Duck: +quack()
```
### ER Diagrams
```
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE_ITEM : contains
PRODUCT ||--o{ LINE_ITEM : "is in"
```
### Inline Edge Styling
Use `linkStyle` to override edge colors and stroke widths — just like [Mermaid's linkStyle](https://mermaid.js.org/syntax/flowchart.html#styling-links):
```
graph TD
A --> B --> C
linkStyle 0 stroke:#ff0000,stroke-width:2px
linkStyle default stroke:#888888
```
| Syntax | Effect |
| ------------------------------- | -------------------------------------- |
| `linkStyle 0 stroke:#f00` | Style a single edge by index (0-based) |
| `linkStyle 0,2 stroke:#f00` | Style multiple edges at once |
| `linkStyle default stroke:#888` | Default style applied to all edges |
Index-specific styles override the default. Supported properties: `stroke`, `stroke-width`.
Works in both flowcharts and state diagrams.
### XY Charts
Bar charts, line charts, and combinations — using Mermaid's `xychart-beta` syntax.
**Bar chart:**
```
xychart-beta
title "Monthly Revenue"
x-axis [Jan, Feb, Mar, Apr, May, Jun]
y-axis "Revenue ($K)" 0 --> 500
bar [180, 250, 310, 280, 350, 420]
```
**Line chart:**
```
xychart-beta
title "User Growth"
x-axis [Jan, Feb, Mar, Apr, May, Jun]
line [1200, 1800, 2500, 3100, 3800, 4500]
```
**Combined bar + line:**
```
xychart-beta
title "Sales with Trend"
x-axis [Jan, Feb, Mar, Apr, May, Jun]
bar [300, 380, 280, 450, 350, 520]
line [300, 330, 320, 353, 352, 395]
```
**Horizontal orientation:**
```
xychart-beta horizontal
title "Language Popularity"
x-axis [Python, JavaScript, Java, Go, Rust]
bar [30, 25, 20, 12, 8]
```
**Axis configuration:**
- Categorical x-axis: `x-axis [A, B, C]`
- Numeric x-axis range: `x-axis 0 --> 100`
- Axis titles: `x-axis "Category" [A, B, C]`
- Y-axis range: `y-axis "Score" 0 --> 100`
**Multi-series:** Add multiple `bar` and/or `line` declarations. Each series gets a distinct color from a monochromatic palette derived from the theme's accent color.
### XY Chart Styling
The chart renderer follows a clean, minimal design philosophy inspired by Apple and Craft:
- **Dot grid** — A subtle dot pattern fills the plot area instead of traditional solid grid lines
- **Rounded bars** — All bar corners are rounded for a modern, polished look
- **Smooth curves** — Line series use natural cubic spline interpolation, producing mathematically smooth curves through all data points (not straight segments or staircase steps)
- **Floating labels** — No visible axis lines or tick marks; labels float freely for a clutter-free aesthetic
- **Drop-shadow lines** — Each line series has a subtle shadow beneath it for depth
- **Monochromatic palette** — Series 0 uses the theme's accent color; additional series get darker/lighter shades of the same hue with subtle hue drift, adapting automatically to light or dark backgrounds
- **Interactive tooltips** — When rendered with `interactive: true`, hovering over bars or data points shows value tooltips. Multi-line tooltips appear when multiple series share an x-position
- **Sparse line dots** — Lines with 12 or fewer data points show data point dots by default for readability
- **Full theme support** — All 15 built-in themes (and custom themes) apply to charts. The accent color drives the entire series color palette
- **Live theme switching** — Chart series colors are CSS custom properties (`--xychart-color-N`), so theme changes apply instantly without re-rendering
---
## ASCII Output
For terminal environments, CLI tools, or anywhere you need plain text, render to ASCII or Unicode box-drawing characters:
```typescript
import { renderMermaidASCII } from 'beautiful-mermaid'
// Unicode mode (default) — prettier box drawing
const unicode = renderMermaidASCII(`graph LR; A --> B`)
// Pure ASCII mode — maximum compatibility
const ascii = renderMermaidASCII(`graph LR; A --> B`, { useAscii: true })
```
**Unicode output:**
```
┌───┐ ┌───┐
│ │ │ │
│ A │────►│ B │
│ │ │ │
└───┘ └───┘
```
**ASCII output:**
```
+---+ +---+
| | | |
| A |---->| B |
| | | |
+---+ +---+
```
### ASCII Options
```typescript
renderMermaidASCII(diagram, {
useAscii: false, // true = ASCII, false = Unicode (default)
paddingX: 5, // Horizontal spacing between nodes
paddingY: 5, // Vertical spacing between nodes
boxBorderPadding: 1, // Padding inside node boxes
colorMode: 'auto', // 'none' | 'auto' | 'ansi16' | 'ansi256' | 'truecolor' | 'html'
theme: { ... }, // Partial — override default colors
})
```
### ASCII XY Charts
XY charts render to ASCII with dedicated chart-drawing characters:
- **Bar charts** — `█` blocks (Unicode) or `#` (ASCII mode)
- **Line charts** — Staircase routing with rounded corners: `╭╮╰╯│─` (Unicode) or `+|-` (ASCII)
- **Multi-series** — Each series gets a distinct ANSI color from the theme's accent palette
- **Legends** — Automatically shown when multiple series are present
- **Horizontal charts** — Fully supported with categories on the y-axis
---
## API Reference
### `renderMermaidSVG(text, options?): string`
Render a Mermaid diagram to SVG. Synchronous. Auto-detects diagram type.
**Parameters:**
- `text` — Mermaid source code
- `options` — Optional `RenderOptions` object
**RenderOptions:**
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `bg` | `string` | `#FFFFFF` | Background color (or CSS variable) |
| `fg` | `string` | `#27272A` | Foreground color (or CSS variable) |
| `line` | `string?` | — | Edge/connector color |
| `accent` | `string?` | — | Arrow heads, highlights |
| `muted` | `string?` | — | Secondary text, labels |
| `surface` | `string?` | — | Node fill tint |
| `border` | `string?` | — | Node stroke color |
| `font` | `string` | `Inter` | Font family |
| `transparent` | `boolean` | `false` | Render with transparent background |
| `padding` | `number` | `40` | Canvas padding in px |
| `nodeSpacing` | `number` | `24` | Horizontal spacing between sibling nodes |
| `layerSpacing` | `number` | `40` | Vertical spacing between layers |
| `componentSpacing` | `number` | `24` | Spacing between disconnected components |
| `thoroughness` | `number` | `3` | Crossing minimization trials (1-7, higher = better but slower) |
| `interactive` | `boolean` | `false` | Enable hover tooltips on XY chart bars and data points |
**XY Charts:** Diagrams starting with `xychart-beta` are auto-detected — no separate function needed. The `accent` color option drives the chart series color palette.
### `renderMermaidSVGAsync(text, options?): Promise`
Async version of `renderMermaidSVG()`. Same output, returns a `Promise`. Useful in async server handlers or data loaders.
### `renderMermaidASCII(text, options?): string`
Render a Mermaid diagram to ASCII/Unicode text. Synchronous.
**AsciiRenderOptions:**
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `useAscii` | `boolean` | `false` | Use ASCII instead of Unicode |
| `paddingX` | `number` | `5` | Horizontal node spacing |
| `paddingY` | `number` | `5` | Vertical node spacing |
| `boxBorderPadding` | `number` | `1` | Inner box padding |
| `colorMode` | `string` | `'auto'` | `'none'`, `'auto'`, `'ansi16'`, `'ansi256'`, `'truecolor'`, or `'html'` |
| `theme` | `Partial` | — | Override default colors for ASCII output |
### `parseMermaid(text): MermaidGraph`
Parse Mermaid source into a structured graph object (for custom processing).
### `fromShikiTheme(theme): DiagramColors`
Extract diagram colors from a Shiki theme object.
### `THEMES: Record`
Object containing all 15 built-in themes.
### `DEFAULTS: { bg: string, fg: string }`
Default colors (`#FFFFFF` / `#27272A`).
---
## Attribution
The ASCII rendering engine is based on [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii) by Alexander Grooff. We ported it from Go to TypeScript and extended it with:
- Sequence diagram support
- Class diagram support
- ER diagram support
- Unicode box-drawing characters
- Configurable spacing and padding
Thank you Alexander for the excellent foundation!
---
## License
MIT — see [LICENSE](LICENSE) for details.
---
Built with care by the team at [Craft](https://craft.do)
================================================
FILE: bench.ts
================================================
/**
* Performance benchmark for beautiful-mermaid.
*
* Runs all sample definitions through both renderers (SVG + ASCII) in Bun
* and prints a table with per-sample timing and aggregate stats.
*
* Usage: bun run packages/mermaid/bench.ts
*/
import { samples } from './samples-data.ts'
import { renderMermaid } from './src/index.ts'
import { renderMermaidAscii } from './src/ascii/index.ts'
// ============================================================================
// Types
// ============================================================================
interface Result {
index: number
title: string
category: string
svgMs: number
asciiMs: number
svgError: string | null
asciiError: string | null
}
// ============================================================================
// Helpers
// ============================================================================
/** Pad/truncate a string to exactly `width` characters, right-aligned if numeric. */
function col(value: string, width: number, align: 'left' | 'right' = 'left'): string {
const truncated = value.length > width ? value.slice(0, width - 1) + '\u2026' : value
return align === 'right' ? truncated.padStart(width) : truncated.padEnd(width)
}
function fmtMs(ms: number): string {
return ms.toFixed(1)
}
// ============================================================================
// Main
// ============================================================================
const results: Result[] = []
const totalStart = performance.now()
console.log(`\nbeautiful-mermaid — Benchmark (${samples.length} samples)`)
console.log('═'.repeat(90))
console.log(
`${col('#', 4, 'right')} ${col('Title', 38)} ${col('Category', 15)} ${col('SVG (ms)', 10, 'right')} ${col('ASCII (ms)', 10, 'right')} ${col('Total', 10, 'right')}`,
)
console.log('─'.repeat(90))
for (let i = 0; i < samples.length; i++) {
const sample = samples[i]!
const category = sample.category ?? 'Other'
let svgMs = 0
let asciiMs = 0
let svgError: string | null = null
let asciiError: string | null = null
// Render SVG (async — uses dagre layout for flowcharts/state/class/ER)
try {
const t0 = performance.now()
await renderMermaid(sample.source, sample.options)
svgMs = performance.now() - t0
} catch (err) {
svgError = String(err)
svgMs = -1
}
// Render ASCII (sync — custom text layout, no dagre)
try {
const t0 = performance.now()
renderMermaidAscii(sample.source)
asciiMs = performance.now() - t0
} catch (err) {
asciiError = String(err)
asciiMs = -1
}
const totalMs = (svgMs >= 0 ? svgMs : 0) + (asciiMs >= 0 ? asciiMs : 0)
results.push({ index: i, title: sample.title, category, svgMs, asciiMs, svgError, asciiError })
// Print row
const svgStr = svgMs >= 0 ? fmtMs(svgMs) : 'ERR'
const asciiStr = asciiMs >= 0 ? fmtMs(asciiMs) : 'N/A'
console.log(
`${col(String(i + 1), 4, 'right')} ${col(sample.title, 38)} ${col(category, 15)} ${col(svgStr, 10, 'right')} ${col(asciiStr, 10, 'right')} ${col(fmtMs(totalMs), 10, 'right')}`,
)
}
const totalElapsed = performance.now() - totalStart
// ============================================================================
// Aggregates
// ============================================================================
console.log('═'.repeat(90))
const svgTimes = results.filter(r => r.svgMs >= 0).map(r => r.svgMs)
const asciiTimes = results.filter(r => r.asciiMs >= 0).map(r => r.asciiMs)
const svgTotal = svgTimes.reduce((a, b) => a + b, 0)
const asciiTotal = asciiTimes.reduce((a, b) => a + b, 0)
console.log(`Total: ${fmtMs(totalElapsed)}ms (SVG: ${fmtMs(svgTotal)}ms, ASCII: ${fmtMs(asciiTotal)}ms)`)
console.log(`Average: ${fmtMs((svgTotal + asciiTotal) / results.length)}ms per sample`)
// Find slowest SVG and ASCII
if (svgTimes.length > 0) {
const slowestSvg = results.filter(r => r.svgMs >= 0).sort((a, b) => b.svgMs - a.svgMs)[0]!
console.log(`Slowest SVG: #${slowestSvg.index + 1} ${slowestSvg.title} (${fmtMs(slowestSvg.svgMs)}ms)`)
}
if (asciiTimes.length > 0) {
const slowestAscii = results.filter(r => r.asciiMs >= 0).sort((a, b) => b.asciiMs - a.asciiMs)[0]!
console.log(`Slowest ASCII: #${slowestAscii.index + 1} ${slowestAscii.title} (${fmtMs(slowestAscii.asciiMs)}ms)`)
}
// Report errors
const svgErrors = results.filter(r => r.svgError)
const asciiErrors = results.filter(r => r.asciiError)
if (svgErrors.length > 0) {
console.log(`\nSVG errors (${svgErrors.length}):`)
for (const r of svgErrors) {
console.log(` #${r.index + 1} ${r.title}: ${r.svgError}`)
}
}
if (asciiErrors.length > 0) {
console.log(`\nASCII unsupported (${asciiErrors.length}):`)
for (const r of asciiErrors) {
console.log(` #${r.index + 1} ${r.title}`)
}
}
// Category breakdown
console.log('\n── By Category ──')
const catMap = new Map()
for (const r of results) {
if (!catMap.has(r.category)) catMap.set(r.category, [])
catMap.get(r.category)!.push(r)
}
for (const [cat, catResults] of catMap) {
const catSvg = catResults.filter(r => r.svgMs >= 0).reduce((a, r) => a + r.svgMs, 0)
const catAscii = catResults.filter(r => r.asciiMs >= 0).reduce((a, r) => a + r.asciiMs, 0)
console.log(` ${col(cat, 16)} ${col(String(catResults.length), 3, 'right')} samples SVG: ${col(fmtMs(catSvg), 8, 'right')}ms ASCII: ${col(fmtMs(catAscii), 8, 'right')}ms Total: ${col(fmtMs(catSvg + catAscii), 8, 'right')}ms`)
}
console.log()
================================================
FILE: dev.ts
================================================
/**
* Development server with live reload for mermaid samples.
*
* Usage: bun run packages/mermaid/dev.ts
*
* - Runs `index.ts` to generate index.html on startup
* - Watches `src/` and `index.ts` for file changes
* - On change, rebuilds index.html and notifies browsers via SSE
* - Serves index.html with an injected live-reload script
*
* This avoids manually re-running the build and refreshing the browser —
* just save a file and the page updates automatically.
*/
import { watch } from 'fs'
import { join } from 'path'
const PORT = 3456
const ROOT = import.meta.dir
// ============================================================================
// Build management
// ============================================================================
let building = false
const sseClients = new Set()
async function rebuild(): Promise {
if (building) return
building = true
console.log('\x1b[36m[dev]\x1b[0m Rebuilding samples...')
const t0 = performance.now()
const proc = Bun.spawn(['bun', 'run', join(ROOT, 'index.ts')], {
cwd: ROOT,
stdout: 'inherit',
stderr: 'inherit',
})
await proc.exited
const ms = (performance.now() - t0).toFixed(0)
if (proc.exitCode === 0) {
console.log(`\x1b[32m[dev]\x1b[0m Rebuilt in ${ms}ms`)
// Notify all connected browsers to reload
for (const client of sseClients) {
try {
client.enqueue('data: reload\n\n')
} catch {
sseClients.delete(client)
}
}
} else {
console.error(`\x1b[31m[dev]\x1b[0m Build failed (exit ${proc.exitCode})`)
}
building = false
}
// ============================================================================
// File watching — debounced to coalesce rapid saves
// ============================================================================
let debounce: Timer | null = null
function onFileChange(_event: string, filename: string | null): void {
// Ignore index.html itself (it's the output, not a source)
if (filename === 'index.html') return
if (debounce) clearTimeout(debounce)
debounce = setTimeout(() => {
console.log(`\x1b[90m[dev]\x1b[0m Change detected${filename ? `: ${filename}` : ''}`)
rebuild()
}, 150)
}
// Watch the entire mermaid package for changes (excludes index.html output)
watch(ROOT, { recursive: true }, onFileChange)
// ============================================================================
// HTTP server
// ============================================================================
// Initial build before starting the server
await rebuild()
console.log(`\x1b[36m[dev]\x1b[0m Server running at \x1b[1mhttp://localhost:${PORT}\x1b[0m`)
console.log(`\x1b[36m[dev]\x1b[0m Watching for changes in src/ and index.ts\n`)
Bun.serve({
port: PORT,
async fetch(req) {
const url = new URL(req.url)
// SSE endpoint — browsers connect here to receive reload signals
if (url.pathname === '/__dev_events') {
let controller!: ReadableStreamDefaultController
const stream = new ReadableStream({
start(c) {
controller = c
sseClients.add(controller)
},
cancel() {
sseClients.delete(controller)
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
}
// Serve index.html with injected live-reload script
const file = Bun.file(join(ROOT, 'index.html'))
if (!(await file.exists())) {
return new Response('index.html not found — build may have failed', { status: 404 })
}
let html = await file.text()
// Inject live-reload client before