Roles and stylesheets
A role is a class name given with class or :::, with or without a classDef. Nodes and edges carry it as merlion-c-{name}, clusters as merlion-cc-{name}. An edge takes a role through its id: a e1@--> b, then class e1 failure. A role says what an element is; the theme and the stylesheet decide how it looks, in every theme at once.
Diagram source
flowchart LR accTitle: Built-in roles on nodes, edges and a cluster req[Request] --> auth{Authorised?} auth -->|yes| handler[Handler] auth e1@-->|no| reject[Reject] subgraph work[background work] job[Enqueue job] e2@--> mail[Send mail] end handler --> job handler --> done[Done] stale[Legacy path] class req accent class done ok class auth warn class reject danger class stale muted class e1 failure class e2 async class work groupBuilt-in roles
Section titled “Built-in roles”Seventeen roles are styled with no stylesheet. merlion-themes.css defines their tones in every theme, and each reads a theme token, so setting --merlion-danger on a theme retunes every danger node and failure edge.
| Role | Applies to | Tone | Dash |
|---|---|---|---|
accent |
Nodes | --merlion-accent |
— |
ok |
Nodes | --merlion-ok |
— |
warn |
Nodes | --merlion-warn |
— |
danger |
Nodes | --merlion-danger |
— |
muted |
Nodes | --merlion-muted |
— |
store |
Nodes | --merlion-store |
— |
series-1 … series-8 |
Clusters | --merlion-series-1 … -8 |
— |
group |
Clusters | — | 6 4 on the box |
failure |
Edges | --merlion-danger |
6 4 |
async |
Edges | — | 6 4 |
A tone tints a node’s fill (14%), colours its border and mixes into its label (75%); on an edge it colours the path, the arrowhead and the label; on a cluster it tints the box (8%) and colours its border and title. Each edge role gets its own arrowhead marker, so a failure arrow is red too. A tone on a cluster never reaches its member nodes.
Automatic tones
Section titled “Automatic tones”Diagrams are coloured with no roles written. Decisions ({…}, {{…}}) take warn, stores ([(…)]) take store, terminals (([…]), ((…)), (((…)))) take ok, and each top-level subgraph takes the next series tone, series-1 to series-8, cycling. Nested subgraphs and edges stay neutral. An automatic tone is the built-in role plus the marker class merlion-auto, so themes, stylesheets and host pages restyle it like any role, and .merlion-c-warn.merlion-auto targets automatic tones alone.
An element with its own class, ::: or style keeps exactly what it asks for. --no-auto-tone, the WASM option autoTone: false, or %%{init: {"merlion": {"autoTone": false}}}%% in the source draws every element untoned.
Diagram source
flowchart LR start([Request]) --> check{Cached?} check -->|yes| cache[(Hint cache)] check -->|no| work subgraph work[Render] parse[Parse] --> layout[Layout] end subgraph out[Publish] page[Page] end layout --> page cache --> pageRole names follow the classDef grammar [A-Za-z_][A-Za-z0-9_-]{0,63}; any other name is dropped with W011. Roles are presentation only: any diagram can put any role on any element.
Stylesheets
Section titled “Stylesheets”A stylesheet is a CSS subset that sets --merlion-* tokens and nothing else. It gives custom roles their tones, retunes the built-in ones and defines themes:
:root { --brand: #0f766e; --merlion-accent: var(--brand); }[data-theme="dark"] { --merlion-bg: #101418; --merlion-fg: #e6e6e6; }.merlion-c-store { --merlion-tone: #b8408f; }.merlion-cc-zone { --merlion-dash: 4 2; }This site’s stylesheet (docs/src/styles/diagrams.css) maps the foundations and the accent to Starlight’s colours and defines the roles every diagram on the site uses beside the built-in ones: input (plum) for where a diagram’s source enters, store (teal) for hints, caches and other kept state, output (green) for what a step produces, style (amber) for stylesheets and theme inputs, and optional for a dashed step that runs only while fuel lasts. The gallery’s roles fixture adds queue (a plum node) and senders (a teal, dashed cluster). Each role carries a light and a dark value:
Diagram source
flowchart LR accTitle: Custom roles from the site stylesheet cron["**cron**<br/>every minute"] --> sched[Scheduler] sched --> q[("**push queue**<br/>at-least-once")] subgraph consumers[consumers] a[APNs] b[FCM] end q --> a & b sched -.-> retry[Retry budget] class cron input class sched accent class q queue class consumers senders class retry optionalWhat the subset accepts
Section titled “What the subset accepts”| Selector | Meaning |
|---|---|
:root |
Base theme |
[data-theme="<t>"] |
Named theme <t> ([a-z][a-z0-9-]{0,31}) |
:root:not([data-theme]) inside @media (prefers-color-scheme: dark) |
Automatic dark theme |
.merlion-c-<name>, .merlion-cc-<name> |
Node and edge role, cluster role |
[data-theme="<t>"] .merlion-c-<name>, … .merlion-cc-<name> |
Role under a named theme |
- Theme selectors set colour tokens,
--merlion-c-<name>-fill/-stroke/-color(re-themes aclassDef),--merlion-strokeand private--<ident>colours usable throughvar(). - Role selectors set
--merlion-toneand--merlion-dashonly. - Values are colour literals (hex,
rgb(),hsl(), named colours,oklab()andoklch()inside sRGB) orvar(--name[, literal])naming a token of the same file, resolved at compile time up to 8 deep. A reference resolves in the theme of its rule. - Rejected, with the rule or declaration dropped: fonts, every other property,
color-mix(),calc(),url(),!important, strings and escapes (W018); undefined names and cycles (W019); other selectors and at-rules (W017). - Limits: 64 KiB, 512 rules, 32 declarations per rule, 16 themes, 256 role selectors, 64 KiB of output; over any of them,
E013.
The full grammar is in svg-output.md; the reasoning is in ADR-0009.
Pages link compiled CSS only
Section titled “Pages link compiled CSS only”A page never links the source stylesheet. merlion css parses it into a typed model and writes page CSS with literal values under fixed selector shapes; nothing from the source passes through as text. Compiling compiled output yields the same bytes.
target/release/merlion css site.css -o site.compiled.css # page CSS: literal values, fixed selector shapesLink it after merlion-themes.css, so its rules win at equal specificity. Inline SVGs render without the stylesheet and follow the page’s cascade, so theme switching still never re-renders. --strict turns W017–W019 into errors; an error writes nothing. E013 exits 3.
With a static-site integration the build compiles it once:
integrations: [merlion({ stylesheet: "src/styles/diagrams.css" })];
// unified pipeline.use(rehypeMerlion, { stylesheet: "diagram.css" })The Astro integration writes the compiled CSS as an asset imported after merlion-themes.css on every page; a refused path, E013 or a failed compile fails the build, and warnings are logged. The rehype plugin exposes it as file.data.merlion.css for the page template to link. Both read the file inside the project root, never through a symbolic link, and refuse anything over 64 KiB before reading it.
Standalone SVG
Section titled “Standalone SVG”An SVG outside a page (a file, an <img>, a GitHub embed, librsvg) sees no page CSS. --css bakes one theme’s values into its presentation attributes and fallbacks, so every renderer draws them:
target/release/merlion render d.mmd --css site.css --theme dark -o d.svg # bake one theme into a standalone SVGtarget/release/merlion render d.mmd --css site.css --auto-dark dark -o d.svg # :root, plus dark under prefers-color-scheme--theme resolves a [data-theme] block over :root; the default is :root alone. --auto-dark adds the named block as a prefers-color-scheme: dark variant, which an <img> embed follows. A name the stylesheet does not define exits 2. A baked SVG never declares a custom property, so a page that inlines it can still theme it. Through WASM, pass compileStylesheet(css, { theme }).palette as the palette render option.
Precedence
Section titled “Precedence”Per property and per element, highest first:
- Node
styleand edgelinkStyleliterals. classDefcolours, through their--merlion-c-{name}-*tokens.- Stylesheet role rules, by specificity, then source order.
- Built-in roles.
- The theme, then the built-in default.
A classDef colour reads var(--merlion-c-{name}-fill, <literal>): unthemed renders, mermaid and GitHub show the author’s literal, and a stylesheet that sets the token re-themes it. I030 flags a source colour that ignores the theme; I033 flags a source literal that masks a stylesheet tone on the same element.