<title>Mermaid to Unicode box art</title>

# Mermaid to Unicode box art

Render a strict Mermaid subset as terminal-style Unicode box art in
<a href="/components/text/vnd.mermaid/mermaid-to-unicode-html.wasm" download><qip-content-size src="/components/text/vnd.mermaid/mermaid-to-unicode-html.wasm"></qip-content-size>
of WebAssembly</a>. It runs locally in your browser and as a CLI.

<style>
.mermaid-examples,
.mermaid-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}
.mermaid-examples {
  margin-block: 1rem;
}
.mermaid-examples button[aria-pressed="true"] {
  color: Canvas;
  background: CanvasText;
}
.mermaid-grid {
  display: grid;
  gap: 1rem;
}
.mermaid-panel {
  display: grid;
  gap: 0.5rem;
  min-width: 0;
}
.mermaid-panel textarea {
  box-sizing: border-box;
  width: 100%;
  resize: vertical;
  font: 0.875rem/1.35 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  tab-size: 2;
}
.mermaid-output {
  box-sizing: border-box;
  min-height: 28rem;
  margin: 0;
  padding: 1rem;
  overflow: auto;
  color: #e6edf3;
  background: #14161c;
  border-radius: 0.5rem;
  font: 0.8125rem/1.2 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important;
  line-height: 1.2;
  font-variant-ligatures: none;
}
.mermaid-output .b  { color: #7d8590; }
.mermaid-output .n  { color: #e6edf3; }
.mermaid-output .e  { color: #6e7681; }
.mermaid-output .el { color: #e3b341; }
.mermaid-output .t  { color: #79c0ff; font-weight: bold; }
.mermaid-output .i  { font-style: italic; }
.mermaid-status {
  min-height: 1.5rem;
}
</style>

<div class="mermaid-examples" aria-label="Example diagrams">
  <button type="button" data-example="flowchart" aria-pressed="true">Flowchart</button>
  <button type="button" data-example="subgraphs" aria-pressed="false">Subgraphs</button>
  <button type="button" data-example="sequence" aria-pressed="false">Sequence</button>
  <button type="button" data-example="state" aria-pressed="false">State</button>
  <button type="button" data-example="class" aria-pressed="false">Class</button>
  <button type="button" data-example="er" aria-pressed="false">Entity relationship</button>
</div>

<div class="mermaid-grid">
  <label class="mermaid-panel">
    <strong>Mermaid source</strong>
    <textarea id="mermaid-input" rows="10" spellcheck="false" autocapitalize="off" autocorrect="off">graph TD
  Start[Request received] --> Auth{Authenticated?}
  Auth -->|yes| Rate{Rate limit OK?}
  Auth -->|no| R401[401 Unauthorized]
  Rate -->|yes| H(Handle request)
  Rate -->|no| R429[429 Too Many Requests]
  H -.-> Log[Audit log]
  H ==> Resp[200 OK]</textarea>
  </label>
  <section class="mermaid-panel" aria-labelledby="mermaid-output-heading">
    <strong id="mermaid-output-heading">Unicode box art</strong>
    <pre id="mermaid-output" class="mermaid-output" aria-live="polite"></pre>
  </section>
</div>

<p class="mermaid-actions">
  <button id="mermaid-copy" type="button">Copy as text</button>
  <button id="mermaid-copy-html" type="button">Copy as HTML</button>
  <span id="mermaid-status" class="mermaid-status" role="status"></span>
</p>

This component supports the demonstrated flowchart, subgraph, sequence, state, class, and ER forms and traps on unsupported syntax.

## Download

- <a href="/components/text/vnd.mermaid/mermaid-to-unicode-html.wasm" download>mermaid-to-unicode-html.wasm</a> — <qip-content-size src="/components/text/vnd.mermaid/mermaid-to-unicode-html.wasm"></qip-content-size>

## CLI

```bash
go install github.com/royalicing/qip@latest
curl -O https://qip.dev/components/text/vnd.mermaid/mermaid-to-unicode-html.wasm

printf '%s\n' \
  'graph TD' \
  '  Start[Request received] --> Auth{Authenticated?}' \
  '  Auth -->|yes| Rate{Rate limit OK?}' \
  '  Auth -->|no| R401[401 Unauthorized]' \
  '  Rate -->|yes| H(Handle request)' \
  '  Rate -->|no| R429[429 Too Many Requests]' \
  '  H -.-> Log[Audit log]' \
  '  H ==> Resp[200 OK]' \
  | qip run mermaid-to-unicode-html.wasm > graph.html
```

## JavaScript

<copy-code>

```js
const mermaidRenderer = await WebAssembly.instantiateStreaming(
  fetch("mermaid-to-unicode-html.wasm"),
);
const encoder = new TextEncoder(), decoder = new TextDecoder();

function renderMermaid(source) {
  const {
    memory,
    input_ptr,
    input_utf8_cap,
    output_ptr,
    render,
  } = mermaidRenderer.instance.exports;
  const input = encoder.encode(source);
  if (input.length > input_utf8_cap()) throw new RangeError("Mermaid input is too large");
  new Uint8Array(memory.buffer, input_ptr(), input.length).set(input);
  const outputSize = render(input.length);
  return decoder.decode(new Uint8Array(memory.buffer, output_ptr(), outputSize));
}

const diagram = `graph TD
  Start[Request received] --> Auth{Authenticated?}
  Auth -->|yes| Rate{Rate limit OK?}
  Auth -->|no| R401[401 Unauthorized]
  Rate -->|yes| H(Handle request)
  Rate -->|no| R429[429 Too Many Requests]
  H -.-> Log[Audit log]
  H ==> Resp[200 OK]`;

document.querySelector("#diagram").innerHTML = renderMermaid(diagram);
```

</copy-code>

The component returns an HTML fragment containing the box art and styling spans. Insert it as HTML to preserve the colors, or use the element's `textContent` when you only need plain Unicode text.

## Notes

Inspired by [Simon Willison’s Mermaid utility](https://tools.simonwillison.net/grok-mermaid).

<script type="module">
import { contentComponent, contentTypeUTF8 } from "/qip-runner.js";

const examples = {
  flowchart: `graph TD
  Start[Request received] --> Auth{Authenticated?}
  Auth -->|yes| Rate{Rate limit OK?}
  Auth -->|no| R401[401 Unauthorized]
  Rate -->|yes| H(Handle request)
  Rate -->|no| R429[429 Too Many Requests]
  H -.-> Log[Audit log]
  H ==> Resp[200 OK]`,
  subgraphs: `flowchart LR
  subgraph Client
    UI[Browser UI] --> SW[Service worker]
  end
  subgraph Server
    API[API gateway] --> DB[Postgres]
  end
  SW -->|HTTPS| API`,
  sequence: `sequenceDiagram
  participant U as User
  participant B as Browser
  participant W as WASM module
  U->>B: Edit mermaid source
  B->>W: wasm_render_html()
  W-->>B: Unicode box art
  B-->>U: Rendered diagram`,
  state: `stateDiagram-v2
  [*] --> Idle
  Idle --> Loading : fetch
  Loading --> Ready : success
  Loading --> Error : failure
  Error --> Loading : retry
  Ready --> [*]`,
  class: `classDiagram
  class Animal {
    +String name
    +makeSound()
  }
  Animal <|-- Dog
  Animal <|-- Cat
  Dog : +fetch()`,
  er: `erDiagram
  CUSTOMER ||--o{ ORDER : places
  ORDER ||--|{ LINE_ITEM : contains
  CUSTOMER {
    string name
    string email
  }`,
};

const input = document.getElementById("mermaid-input");
const output = document.getElementById("mermaid-output");
const copyButton = document.getElementById("mermaid-copy");
const copyHtmlButton = document.getElementById("mermaid-copy-html");
const status = document.getElementById("mermaid-status");
const exampleButtons = [...document.querySelectorAll("[data-example]")];
const mermaid = contentTypeUTF8("text/vnd.mermaid");
const html = contentTypeUTF8("text/html");
const componentModule = await WebAssembly.compileStreaming(
  fetch("/components/text/vnd.mermaid/mermaid-to-unicode-html.wasm"),
);
const renderMermaid = contentComponent(mermaid, componentModule, html);

function render() {
  try {
    output.innerHTML = renderMermaid(input.value);
    status.textContent = "";
  } catch {
    output.replaceChildren();
    status.textContent = "Unsupported or malformed Mermaid input.";
  }
}

let renderFrame = 0;
input.addEventListener("input", () => {
  for (const button of exampleButtons) button.setAttribute("aria-pressed", "false");
  cancelAnimationFrame(renderFrame);
  renderFrame = requestAnimationFrame(render);
});

for (const button of exampleButtons) {
  button.addEventListener("click", () => {
    input.value = examples[button.dataset.example];
    for (const candidate of exampleButtons) {
      candidate.setAttribute("aria-pressed", String(candidate === button));
    }
    render();
  });
}

function showCopySucceeded(button) {
  button.classList.remove("succeeded");
  void button.offsetWidth;
  button.classList.add("succeeded");
}

for (const button of [copyButton, copyHtmlButton]) {
  button.addEventListener("animationend", () => {
    button.classList.remove("succeeded");
  });
}

async function copyOutput(button, value) {
  try {
    await navigator.clipboard.writeText(value);
    status.textContent = "";
    showCopySucceeded(button);
  } catch {
    status.textContent = "Copy failed.";
  }
}

copyButton.addEventListener("click", () => {
  copyOutput(copyButton, output.textContent);
});

copyHtmlButton.addEventListener("click", () => {
  copyOutput(copyHtmlButton, output.innerHTML);
});

render();
</script>
