learn-mermaid-with-phoebe / Deep dive 6.3 of 3
Learn Mermaid with Phoebe · Deep dive 6.3 of 3

Under the hood: directives, icons, layouts, math

The last ten percent that makes mermaid feel professional: config that layers predictably, real logos in your architecture diagrams, a smarter layout engine for hairy graphs, and equations in labels - plus the security dial that governs it all.

🔴 Self-paced Data & AI teams ~35 min self-paced
Config layers Icons Layout engines Math + security
Part 0

Why this deep dive exists

Everything so far has been about WHAT you draw. This deep dive is about the machine that draws it: how configuration flows from site defaults down to a single diagram, how to bring real icons and better layout engines into play, and where the security model draws its lines. Honest framing: two features here (ELK layout and KaTeX math) need extra setup beyond a plain CDN script tag - we show the source and say so, instead of pretending. Everything else on this page renders live.

Live - renders with plain CDN mermaid@11 Shown as source - needs extra setup ★ Try it now This page's own config is the demo
★ What you walk out with A mental model of config precedence (defaults, then initialize(), then per-diagram frontmatter - with secure keys locked at site level), one diagram flipped to dark theme while the rest of the page stays pink, an architecture diagram with icons, the rule of thumb for when ELK beats dagre, KaTeX-in-labels syntax, and a working understanding of securityLevel - including why click links need "loose".
Part 1 · covers "Configuration" + "Directives"

Config layers: who wins 9 min

Mermaid builds each diagram's render config from three sources, applied in order: built-in defaults, then the site-wide mermaid.initialize() call, then per-diagram config from the author (frontmatter, or the older %%{init}%% directives). Later layers override earlier ones - except a short list of secure keys that only the site owner controls.

Each layer overrides the one before it 1 · Defaults built into mermaid theme, fonts, sizes 2 · initialize() site owner, once per page this page: pink theme 3 · Per diagram frontmatter config (or legacy directives) Render config Winner on any normal key: the per-diagram layer. The diagram author gets the last word... ...except SECURE keys: securityLevel, startOnLoad and friends stay pinned at the initialize() level. A diagram pasted into your page cannot loosen your page's security. That is the whole design. Directives (%%{init}%%) sit in layer 3 too - deprecated since v10.5 in favor of frontmatter, still parsed everywhere.
🔍 Click to zoom - the precedence ladder: defaults → initialize() → per-diagram frontmatter → render config, with secure keys locked at step 2.
Self-studyFrontmatter: one diagram goes dark3 min▶

Frontmatter is a YAML block between --- fences at the very top of the diagram. Its config: key can override almost the entire configuration for that one diagram. Proof, live: this page's initialize() sets the pink theme, and the diagram below flips itself to the dark theme anyway - while every other diagram on the page stays pink.

Frontmatter flips one diagram to dark theme SOURCE RENDERS AS --- config: theme: dark --- flowchart LR ev["App events"] --> ing["Ingestion"] ing --> dw[("Warehouse")] dark theme, this diagram only App events Ingestion Warehouse Frontmatter config overrides one diagram only - ten diagrams, one frontmatter block each, zero interference.
🔍 Click to zoom - frontmatter flips one diagram, not the whole page
---
config:
  theme: dark
---
flowchart LR
  ev["App events"] --> ing["Ingestion"]
  ing --> dw[("Warehouse")]
  dw --> bi["Dashboards"]
  • The order matters: frontmatter must be the FIRST thing in the block - the --- fence before the diagram keyword.
  • title lives here too: title: My diagram in frontmatter is the modern way to caption a diagram.
  • Scope is one diagram. Ten diagrams on a page, one frontmatter block each, zero interference.
Self-studyDirectives: the legacy dial (still everywhere)2 min▶

Before frontmatter existed, per-diagram config went in a %%{init: {...}}%% directive on the first line. Deprecated since v10.5, but still parsed - and you WILL meet it in older READMEs and Stack Overflow answers, so read it fluently:

%%{init: { "theme": "forest" } }%%
flowchart LR
  a["Same trick"] --> b["older syntax"]
  b --> c["still parses in v11"]
Translation rule %%{init: { "theme": "forest" } }%% is exactly --- config: theme: forest --- in frontmatter. When you touch an old diagram, migrate the directive while you're there - same layer, same precedence, nicer diff.
Self-studyPer-diagram overrides you'll actually use2 min▶

Theme is the party trick; the workhorse overrides are diagram-type settings. Nesting under config: mirrors the config schema - here, flowchart edges become right-angled steps for that circuit-board look, on this one diagram only:

---
config:
  flowchart:
    curve: step
---
flowchart LR
  raw["Raw zone"] --> stg["Staging"]
  stg --> mart["Marts"]
  stg --> ml["Feature store"]
★ Try it nowFlip this block to Code, open it on mermaid.live, and swap step for basis, then linear. Three renders, three personalities, same meaning - per-diagram config is how one doc supports them all.
Part 2 · covers "Registering icon packs" + architecture icons

Icons: logos in your boxes 7 min

Architecture diagrams ship with five built-in icons - cloud, database, disk, internet, server - and those render anywhere mermaid does. The upgrade is registering an iconify pack (200,000+ icons, including every vendor logo you deploy) so your warehouse box can wear the actual Snowflake or Redshift mark.

Self-studyThe built-ins, live: Nova as an architecture diagram3 min▶

Architecture diagrams place service nodes (each with an icon) inside group boxes, and wire them edge-to-edge with side anchors: L, R, T, B. The five default icons need no registration at all:

Built-in icons need zero registration SOURCE RENDERS AS architecture-beta group nova(cloud)[Nova platform] service ing(server)[Ingestion] in nova service dw(database)[Warehouse] in nova ing:R -- L:dw Nova platform Ingestion Warehouse Storage The five built-in icons need no registration - only a self-hosted docs page can add real vendor logos.
🔍 Click to zoom - five built-in icons need zero registration
architecture-beta
  group nova(cloud)[Nova platform]
  service ing(server)[Ingestion] in nova
  service dw(database)[Warehouse] in nova
  service lake(disk)[Object storage] in nova
  service web(internet)[Dashboards]
  ing:R -- L:dw
  dw:B -- T:lake
  dw:R -- L:web
  • Icon goes in parentheses, label in brackets: service dw(database)[Warehouse].
  • Edges anchor to sides: ing:R -- L:dw means "ingestion's right side to warehouse's left side" - you're steering geometry, not just topology.
  • Groups nest services with in group_id - the platform boundary draws itself.
Self-studyRegistering real icon packs (self-hosted power)3 min▶

On a page you control, register any iconify pack once and reference its icons as packname:icon-name in architecture diagrams (and flowchart icon shapes). The docs' own recipe, fetching the logos pack from a CDN:

Site setup - runs next to mermaid.initialize()mermaid.registerIconPacks([ { name: "logos", loader: () => fetch("https://unpkg.com/@iconify-json/logos@1/icons.json") .then((res) => res.json()), }, ]);

Then in any architecture diagram on that page:

Usage - icon names become logos:whateverarchitecture-beta service dw(logos:aws-redshift)[Warehouse] service orch(logos:airflow)[Orchestration]
Honest portability note Registered packs are a JavaScript-side setup, so they work where YOU run mermaid: your docs site, your wiki, this course's pages. GitHub's built-in renderer registers nothing beyond the defaults - push a diagram using logos:airflow to a README and you'll get a placeholder, not the logo. Design READMEs around the five built-ins; save vendor logos for self-hosted pages.
Real world

A platform team's internal docs site registered the logos pack in its mermaid setup. Every architecture diagram suddenly showed the real Kafka, Snowflake and Airflow marks - and new joiners stopped asking "which box is the warehouse?" The same diagrams in GitHub fall back to plain boxes: readable there, gorgeous on the docs site, one source for both.

Part 3 · covers "Layouts"

Layout engines: dagre vs ELK 7 min

Every flowchart so far was laid out by dagre, mermaid's default engine - fast, fine, occasionally spaghetti. ELK (Eclipse Layout Kernel) is the heavyweight alternative: better edge routing, fewer crossings, calmer results on big graphs. Switching is one line of frontmatter... where ELK is available.

Self-studyOne line to switch - and where it works3 min▶

The switch itself is beautifully boring:

Shown as source - ELK isn't bundled with plain CDN mermaid, so this block isn't live here--- config: layout: elk --- flowchart TD raw["Raw events"] --> val["Validation"] raw --> enr["Enrichment"] crm["CRM export"] --> val val --> dw[("Warehouse")] enr --> dw dw --> bi["Dashboards"] dw --> ml["Feature store"] bi --> alerts["Alerting"] ml --> alerts
Honest availability note ELK ships as a separate package (@mermaid-js/layout-elk) that self-hosted pages must register alongside mermaid - which is why the block above is source, not live: this course's plain CDN setup doesn't include it. mermaid.live has ELK built in, so paste the block there and it just works. Other supported layouts in the docs: dagre (default), elk, tidy-tree, cose-bilkent.
Self-studyWhen ELK earns its keep2 min read▶
  • Many edge crossings - the classic data lineage graph where every source feeds every mart. ELK's routing untangles what dagre leaves knotted.
  • Long ranks - wide layers (20 tables in one tier) that dagre spreads into a football field. ELK packs them more sensibly.
  • Dense subgraph nesting - multiple zones with cross-zone edges tend to route cleaner under ELK.
  • Not worth it below ~15 nodes: for small diagrams the engines produce near-identical results, and dagre needs zero extra setup. Try dagre first, reach for ELK when the diagram visibly fights you.
Real world

A 40-table lineage diagram for a warehouse migration was unreadable under the default layout - edges everywhere. One frontmatter line switching to ELK on mermaid.live turned it into something the team could actually walk through in the review meeting. Same source, different brain doing the geometry.

Part 4 · covers "Math configuration" + securityLevel

Math in labels, and the security dial 8 min

Two under-the-hood topics that share a theme: what mermaid will and won't render for you. KaTeX puts real equations in flowchart and sequence labels; securityLevel decides how much your diagrams are allowed to do - including whether click links work at all.

Self-studyKaTeX: equations in labels3 min▶

Wrap an expression in $$...$$ inside a quoted label and mermaid hands it to the KaTeX typesetter (v10.9+). Supported in flowcharts and sequence diagrams - a lovely fit for ML pipeline docs where the transformation IS a formula:

Shown as source - KaTeX needs extra setup beyond this page's plain CDN script, so this block isn't live hereflowchart LR feat["Features x"] --> score["$$P(churn) = \sigma(w^T x + b)$$"] score --> gate{"$$score \geq 0.9$$"} gate -->|yes| act["Retention flow"] gate -->|no| skip["No action"]
Honest availability note Math rendering needs KaTeX available to mermaid - mermaid.live has it wired up, so the block above works there as-is. This course's minimal CDN setup doesn't include it, and GitHub's renderer doesn't do math either. Also budget for cross-platform quirks: by default mermaid renders math via the browser's MathML, which differs between browsers; the forceLegacyMathML config plus KaTeX's stylesheet gives consistent output when that matters.
Self-studysecurityLevel: the dial behind everything3 min▶

Diagrams are user input, and mermaid treats them that way. The securityLevel setting - a SECURE key, so only initialize() can set it, never a pasted diagram - has four positions:

LevelWhat it meansUse when
strictdefault - HTML in labels is encoded, click interactions disabledrendering diagrams you didn't write (the safe default; this page uses it)
looseHTML allowed, click links and callbacks enabledtrusted content where you want clickable diagrams
antiscriptHTML allowed but script tags strippeda middle ground for mostly-trusted content
sandboxeverything renders inside a locked-down iframemaximum paranoia; some interactive features stop working

The everyday symptom: click statements that silently do nothing. A flowchart like this needs the SITE to be at loose - no frontmatter can rescue it, by design:

Does nothing under strict (this page's level) - needs securityLevel loose in initialize()flowchart LR dash["Revenue dashboard"] click dash "https://bi.nova.example/revenue" _blank
Why the lock exists If a diagram could set its own securityLevel, any pasted diagram could grant itself script access - the lock is what makes "render diagrams from anywhere" a safe feature. When click links matter (a lineage map linking to your catalog), you own the page, you set loose consciously, and you only render trusted sources on it.
Homework

Try it yourself - this week ◐ 35-45 min total

Source material

Official docs covered

This deep dive works through these sections of the official docs at mermaid.js.org (mermaid 11.x): the configuration, directives, icons, layouts and math pages under Config, plus the securityLevel setting from the usage guide.

✓Configuration + directivesPart 1 - three layers, precedence, frontmatter, %%{init}%% legacy
✓Registering icon packsPart 2 - built-in five, registerIconPacks, iconify, portability
✓Layouts (ELK)Part 3 - layout: elk, when it beats dagre, availability
✓Math configurationPart 4 - $$...$$ in flowchart/sequence, MathML vs legacy KaTeX
✓securityLevelPart 4 - strict/loose/antiscript/sandbox, why click needs loose
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · The site's initialize() sets the pink theme. One diagram carries frontmatter with config: theme: dark. What renders?

Defaults, then initialize(), then per-diagram frontmatter - the author gets the last word on everything except secure keys. You watched it happen in Part 1.

2 · Your 40-node lineage flowchart is spaghetti. How do you hand layout to ELK?

layout: elk in frontmatter config is the whole switch. It works out of the box on mermaid.live; self-hosted pages must register @mermaid-js/layout-elk first.

3 · You added click statements to a flowchart on the docs site and nothing happens. Most likely cause?

securityLevel is a secure, site-level key: strict disables interactions by design, and only the initialize() call can loosen it. That lock is what makes rendering untrusted diagrams safe.

Deep dive 6.3 cheat sheet · pin this

Config layersdefaults → initialize() → per-diagram frontmatter/directives → render config. Later wins, secure keys excepted.
Frontmatter skeleton--- config: theme: dark --- as the FIRST lines of the block. Also home for title: and diagram-type settings.
Directives (legacy)%%{init: { "theme": "forest" } }%% - deprecated since v10.5, still parsed. Migrate to frontmatter when you touch one.
IconsBuilt-ins render everywhere: cloud, database, disk, internet, server. registerIconPacks unlocks iconify (logos:airflow) on pages you host.
ELK switchconfig: layout: elk in frontmatter. Built into mermaid.live; self-hosted needs @mermaid-js/layout-elk. Reach for it on 15+ node graphs with crossings.
Math"$$...$$" inside quoted labels, flowchart + sequence only (v10.9+). Needs KaTeX available; forceLegacyMathML for consistent output.
securityLevelstrict (default, safe) · loose (click works) · antiscript · sandbox. Secure key: initialize() only, never per diagram.
Debug order for "config ignored"Is it a secure key? → is frontmatter the first line? → valid YAML? → right nesting under config: ?