You write a diagram in a page; you color it in your repository. Theming Mermaid takes exactly two files, and they change together: src/static/js/mermaid.theme.json is the palette, and tailwind/docs/mermaid.css is for the handful of colors the palette cannot reach. Neither file can read the other, and nothing enforces that they agree. Both absent is the normal case and costs nothing — diagrams then render in Mermaid’s own colors.
Everything below is a method you apply to your own brand. leed.ai’s two files are the worked example, and a customer who follows the method ends up with their palette rather than a copy of Leed’s.
Which file a color belongs in — the test, not the taste
Take the decision first, because it is mechanical and it will save you the rest of this page when you arrive with one wrong color.
A gap belongs in the JSON if any themeVariables key reaches it, and in the CSS only if none does.
The diagnostic that settles it without reading Mermaid’s source: render the diagram with every color variable forced to one sentinel color. If the shape moves, it is themeable and belongs in the JSON. If it does not, it is a hardcoded literal and belongs in the CSS.
That test is worth running. Applied to a 547-line reference stylesheet of 86 rule blocks, it kept nine: sixty-one matched nothing or fought a value the palette already reached, and sixteen had a better fix in the JSON — a theme variable the reference’s author did not know existed. It also surfaced one gap the reference had never covered. A rule that matches, reads correctly and paints nothing is worse than no rule, because the next reader believes it.
Why almost every value can be one of your own tokens
Mermaid renders the SVG inline into the document — the browser script assigns diagram.innerHTML = svg — never as an <img src="data:…">. An inline SVG is part of the page, so the browser resolves every var(--token) inside it against :root in the live cascade. Diagrams therefore follow light and dark on their own, and a palette written almost entirely in your own tokens is a single palette rather than two.
This was measured rather than assumed, because a reader will not take it on trust: var() resolves in all three places Mermaid can put a color — an SVG presentation attribute (<rect fill="var(--card)">), an inline style attribute, and Mermaid’s own <style> block. That is why the ER row bands, which the sketch renderer writes with setAttribute("fill", …), are already your tokens with no rule at all.
Write the palette against site tokens, never docs-only tokens
--docs-* tokens are declared inside an @utility, so they exist only on an element carrying the color-theme class — the documentation <html> and nothing else. A mermaid fence works on any markdown page type. A palette written against --docs-* renders unstyled in a blog or resource page, with no warning and no error.
Use the site-level names declared on bare :root. leed.ai’s palette names thirteen of them — --fg, --fg-muted, --card, --bg, --bg-secondary, --border, --border-strong, --primary, --primary-soft, --error, --error-soft, --success and --success-soft — and nothing else. The catalog of what is available is the site token contract.
Where var() stops working
Mermaid derives colors from colors, and that arithmetic runs on the string it was handed. There are three distinct failure modes and they look nothing alike.
| Mode | Mechanism | What you see |
|---|---|---|
| A — a throw | The color library raises Unsupported color format: "var(--x)" | The diagram renders as raw source text. If it happens while the theme is being computed, every diagram on the page does |
| B — an invalid attribute | The value lands in an SVG presentation attribute, where a custom property is not legal | The element silently takes the initial value: fill goes black, stroke goes to none — invisible. No error |
| C — a sanitizer reject | The railroad renderer runs a regex allowlist over color values | Silently substitutes Mermaid’s own stock color |
The eleven that must be literal hex
This is the entire reason a palette has separate light and dark blocks. Freeze these eleven per scheme:
| Key | Why a var() fails here | Mode | What you see |
|---|---|---|---|
background | Every venn diagram runs a darkness test over it | A | Venn renders as raw source |
primaryColor | Unset venn1/venn4/venn5/venn7 are derived from it by hue rotation | A | Venn renders as raw source |
secondaryColor | Same derivation, for venn2/venn6/venn8 | A | Venn renders as raw source |
tertiaryColor | Derives venn3, and every ER diagram fades it | A | ER and venn render as raw source |
mainBkg | Block-diagram styles fade it; ER cardinality markers paint it as an attribute | A + B | Block renders as raw source |
edgeLabelBackground | Flowchart styles fade it — the most common diagram on any site | A | Every flowchart renders as raw source |
quadrant1Fill | A lighten branch always executes once quadrantPointFill is set | A | Quadrant charts render as raw source |
clusterBkg | Block styles fade it; swimlane title rects paint it as an attribute | A + B | Block diagrams render as raw source |
clusterBorder | Same two paths | A + B | Block diagrams render as raw source |
primaryTextColor | The railroad sanitizer reads it, and it is the fallback for three quadrant text fills | C | Railroad text goes black |
sequenceNumberColor | Not literal-bound — it is a guard; see below | — | Every diagram loses its colors |
The literals still follow the reader’s system scheme, because the browser re-initializes Mermaid and redraws on a prefers-color-scheme change. That is the whole job of the light and dark blocks.
Your file will usually carry more than eleven per-scheme values, and that is expected: anything Mermaid paints as a presentation attribute has to be frozen for the same reason, even though nothing derives from it. leed.ai’s blocks carry twenty-one each — the eleven above, plus the event-model fills and the Wardley evolution color, which are mode-B values.
fontFamily is a literal font stack, never a var()
The reason is layout, not typeface: Mermaid measures rendered text to size its nodes, so an unresolved variable breaks the geometry rather than the color. The railroad renderer additionally rejects any family string containing parentheses, which var( fails on its face.
So the stack is hand-duplicated from documentationConfiguration.font and must be changed whenever that changes. Nothing warns you, which is exactly why it drifts. The stacks for the shipped families, to copy verbatim:
{
"fontFamily": "\"Noto Sans\", ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\""
}Swap the leading quoted name for "Open Sans", "Inter" or "Geist" and leave the rest of the stack alone; the four sans families ship with the same fallback chain.
Two variables that look redundant and are load-bearing
noteBorderColor and sequenceNumberColor are not set for their own color. Each exists to short-circuit a fallback before it runs on a var():
sequenceNumberColorguardssequenceNumberColor || invert(lineColor), andlineColoris a token.noteBorderColorguardsnoteBorderColor || mkBorder(noteBkgColor), andnoteBkgColoris a token.
The shape of the palette file
The file lives at src/static/js/mermaid.theme.json — static/js/, not static/javascript/. Both directories exist in a Leed site and choosing the wrong one fails silently, because a file that is not there is indistinguishable from a site that never wanted a palette.
Four file shapes are accepted.
- shared + light + dark
- shared only
- light + dark
- a bare Mermaid config
The full form, and what you want once any value has to differ between schemes.
{
"shared": { "theme": "base", "themeVariables": { "lineColor": "var(--fg-muted)" } },
"light": { "themeVariables": { "background": "#ffffff" } },
"dark": { "themeVariables": { "background": "#171717" } }
}shared is merged under each scheme, so light mode sees every shared variable plus its own eleven literals.
Identical to writing no scheme keys at all: one palette, used in both schemes. Correct for a palette written entirely in var(), which follows the page on its own.
{
"shared": { "theme": "base", "themeVariables": { "lineColor": "var(--fg-muted)" } }
}Two complete palettes with nothing in common. Legal, and almost always a mistake — you now maintain every variable twice.
{
"light": { "theme": "base", "themeVariables": { "lineColor": "#525252" } },
"dark": { "theme": "base", "themeVariables": { "lineColor": "#a1a1a1" } }
}Naming only one of the two is also accepted: the build emits the same palette for both schemes rather than letting the other fall back to Mermaid’s colors, because a setting that took in one scheme and not the other looks broken.
No shared, light or dark key anywhere. The whole file is taken as one Mermaid config and used in both schemes.
{
"theme": "base",
"themeVariables": { "lineColor": "var(--fg-muted)" }
}The two forms can never be confused, because Mermaid’s own config has no top-level light or dark key — dark is a value of theme, not a key.
shared merges under each scheme one level deep, and the three consequences all matter:
themeVariablesmerges key by key. A name set in bothsharedandlighttakes the light value in light mode. That is the mechanism the eleven literals rely on.- Nothing deeper than one level merges.
themeVariables.xyChart,.radar,.cynefinand.packetare objects insidethemeVariables, so a scheme block setting one would replace the shared one wholesale rather than merging into it. Put nested objects insharedonly. - A sibling of
themeVariablesmerges too.railroad,flowchart,sequenceandlookare top-level Mermaid config keys, so a per-schemerailroadblock is legal and merges cleanly.
"theme": "base" is mandatory, and it sits beside themeVariables
Without it, Mermaid ignores the entire palette. It belongs next to themeVariables, not inside it:
{
"shared": {
"theme": "base",
"themeVariables": { "primaryTextColor": "var(--fg)" }
}
}It is also load-bearing for the companion stylesheet. Mermaid’s old lavender literals — #ECECFF and #9370DB — still exist in 11.17.2, but only inside its default theme, which base never instantiates. They therefore cannot be emitted, and no CSS needs to hunt for them. Switching to "theme": "default" brings them all back and breaks a stylesheet written on the assumption that they cannot appear.
A scheme block takes any Mermaid config key
The whole scheme object is spread into mermaid.initialize(), so anything Mermaid’s config accepts is legal beside themeVariables.
The one that earns its own paragraph is railroad. It is the only way to theme railroad and ABNF diagrams: their color sanitizer rejects var() outright and falls back to stock black, gray and purple, and it does so silently. leed.ai carries a thirteen-key railroad block in each scheme:
{
"light": {
"railroad": {
"terminalFill": "#fafafa",
"terminalStroke": "#e5e5e5",
"terminalTextColor": "#171717",
"nonTerminalFill": "#ffffff",
"nonTerminalStroke": "#e5e5e5",
"commentFill": "#f5f5f5",
"commentStroke": "#e5e5e5",
"commentTextColor": "#525252",
"specialFill": "#f5f5f5",
"specialStroke": "#e5e5e5",
"lineColor": "#525252",
"markerFill": "#525252",
"ruleNameColor": "#171717"
}
}
}Nested objects inside themeVariables
packet, radar, cynefin and xyChart each take an object of their own, and Mermaid preserves any key you pass whether or not its base theme declares it. This is underused and high value — packet.labelColor is the difference between a packet diagram with black text and one that reads in dark mode.
{
"packet": {
"labelColor": "var(--fg)",
"titleColor": "var(--fg)",
"blockStrokeColor": "var(--border-strong)",
"blockFillColor": "var(--bg-secondary)"
}
}When the palette is wrong
It never fails the build and never breaks the page. Diagram colors are not worth a red build, so every failure warns and carries on.
| Message | Where | Cause | Fix | Build fails? |
|---|---|---|---|---|
static/js/mermaid.theme.json is not valid JSON, so diagrams keep Mermaid's own palette: <error> | Build log | A trailing comma, a comment, a stray quote | JSON is strict — no comments, no trailing commas | No |
static/js/mermaid.theme.json must be a JSON object, so diagrams keep Mermaid's own palette | Build log | The file’s top level is an array or a scalar | Wrap it in { } | No |
static/js/mermaid.theme.json is empty, so diagrams keep Mermaid's own palette | Build log | The file parses to {} | Delete it or fill it | No |
static/js/mermaid.theme.json: "shared", "light" and "dark" must each be a Mermaid config object | Build log | One of the three keys holds a string, an array or null | Each must be an object | No |
Theming Mermaid diagrams from static/js/mermaid.theme.json (shared + light + dark) | Build log | Success — it names which keys it used | — | — |
Mermaid rejected this site's palette from static/js/mermaid.theme.json; rendering in Mermaid's own colors instead. | Browser console | A var() in a variable Mermaid derives from | Freeze that variable per scheme | No |
The browser half is a probe. Before drawing anything, the page renders a two-node diagram with your palette applied. If that throws, the console carries the message above and everything re-initializes unthemed — deliberately, because reacting to the first real render failure cannot tell a bad palette from one badly written diagram, and guessing wrong strips the colors from every other diagram on the page.
The probe has a blind spot worth knowing. It renders a flowchart. A var() that only kills one diagram family — block diagrams are the usual one — sails straight through it. Each of those diagrams then fails alone, is logged by itself, and is left as raw source while the rest of the page stays correctly themed.
Reading the error
This is the diagnostic that saves the most time on this page. When one whole diagram renders as raw source, that type rejected one of your var() values, and the error names the first token it choked on, not all of them.
Bisect on the message. Convert the named variable to a literal, rebuild, read the next message, repeat. That finds the minimum set that has to become literals. The alternative — converting every variable that holds a token — works, and costs you live scheme switching on all of them. One measured case cost two variables where the first guess had been twenty-five.
How the palette reaches the page
The feature spans two runtimes and an event.
sequenceDiagram
autonumber
participant B as leed site build
participant P as The page
participant M as Mermaid
B->>B: read src/static/js/mermaid.theme.json
B->>P: emit window.leedMermaidTheme = {light, dark}
Note over P: the module loads only if the page<br/>contains a pre.mermaid element
P->>M: initialize({theme, ...scheme, startOnLoad: false})
P->>M: probe render, two nodes
M-->>P: ok
P->>M: render each diagram from its cached source
Note over P,M: later, the reader's system switches to dark
P->>M: initialize() again with the dark scheme
P->>M: redraw every diagram, no reload
Three details follow from that sequence. startOnLoad: false is applied last and cannot be overridden — the manager renders the diagrams itself, and a site turning it back on would draw every diagram twice. Your theme key is applied before it and both can and should be set. And because each diagram’s original source is kept, a prefers-color-scheme change re-themes and redraws without a reload.
That last point is why the eleven frozen literals are not a compromise: they are per-scheme, and the scheme is re-applied live.
The companion stylesheet, and how much weight a rule needs
The CSS half is a cascade problem, and teaching it as one is the only way to make it transferable.
Mermaid compiles its per-diagram stylesheet under a #<svgId> namespace and inserts it inside the <svg>. Every Mermaid rule therefore carries at least an id. Four paint sources, ranked:
| Rank | Source | Example | What beats it |
|---|---|---|---|
| 4 | An inline style with !important | C4 shapes | Nothing. No author stylesheet reaches it |
| 3 | Mermaid’s own id-scoped <style> | .face { … } | Only your !important |
| 2 | A plain inline style attribute | .actor-man | Only your !important |
| 1 | A rule in your stylesheet | — | Ranks 2, 3 and 4 |
| 0 | A presentation attribute | stroke="#666" | Any declaration at all |
Rank 3 is the trap. Your selector matches, your value is right, and nothing happens, because an id outranks anything an author stylesheet can say about that property on that element.
The working rule that follows: a plain declaration is correct only where Mermaid’s stylesheet says nothing about that property on that element. And the discipline that follows from that: every !important in the file is there because it was measured to be necessary, and every rule without one was measured to win without it. Do not add or remove one on intuition — add the sentinel, render, and read the computed value.
Import this file unlayered
/* tailwind/site.config.css */
@import "./docs/mermaid.css"; /* no layer(...) — deliberately */Mermaid’s <style> lives inside the SVG and is unlayered author CSS, and unlayered CSS beats every cascade layer on normal declarations regardless of specificity. Putting your rules in layer(components) would place them a step behind Mermaid’s own the moment one of them names a selector you also use — which at 11.17.2 is the common case rather than the rare one.
This is the exact opposite of the advice for an alert override, which must be layered, and the single rule that explains both is on cascade layers and overriding Leed. Alerts are Leed’s own components in layer(components); Mermaid’s stylesheet is not layered at all, and you match the thing you are trying to beat.
One order note: the SVG’s <style> is inserted at render time, after your stylesheet is parsed, so on a specificity tie Mermaid wins. There is no tie today, because every Mermaid rule carries an id — but do not write a rule that depends on source order.
Scope every rule to pre.mermaid svg
One selector shape, two reasons. It keeps diagram rules off the rest of the page, and it is what makes them survive click-to-zoom: the zoom stage clones the diagram’s <pre> as a carrier, so a rule scoped this way matches in both places and no second selector is needed.
Coverage, type by type
Every diagram type themes. The three exceptions below are about which file reaches a color, not about whether color is possible at all.
Measured at Mermaid 11.17.2 in a real browser: every type below rendered on one page, and the painted fill and stroke of every element in every SVG was read back and compared against the site’s tokens. “Themed by the palette” means the JSON alone was enough. “Needs a CSS rule” means it was not, and tailwind/docs/mermaid.css carries the rule that finishes it.
With both halves in place every type below is brand. The split matters only if you are porting this to another site, or debugging one: the palette cannot reach the second group, and no amount of adding variables to the JSON will change that. Mermaid derives colors from colors — handed primaryColor it computes borders and secondaries by lightening and darkening the string it was given — and that arithmetic cannot consume a var(). The CSS can, because the browser resolves it after Mermaid has finished.
| Type | Themed by the palette | Needs a CSS rule | Notes |
|---|---|---|---|
| flowchart / graph | Yes | No | edgeLabelBackground must be a per-scheme literal |
| classDiagram | Yes | No | Node fill routes through mainBkg |
| gantt | Yes | No | Critical and done tasks read your error and success tokens |
| gitGraph | Yes | No | Branch colors come from the categorical ramp |
| mindmap | Yes | No | No non-token color found in the render |
| block-beta | Yes | No | Throws unless clusterBkg/clusterBorder are literals |
| packet-beta | Yes | No | Set the nested packet object or the labels are black |
| kanban | Yes | No | Looks unthemed and is not — see below |
| swimlane-beta | Yes | No | No non-token color found in the render |
| ishikawa-beta | Yes | No | No non-token color found in the render |
| sequenceDiagram | Mostly | Yes | g.actor-man carries an inline lavender; the autonumber badge takes the arrowhead’s ink |
| stateDiagram / -v2 | Mostly | Yes | Node fill routes through stateBkg, but the [*] markers stay #ececff / #9370db |
| erDiagram | Mostly | Yes | Attribute row bands are Mermaid’s lavender pair, not attributeBackgroundColor* |
| requirementDiagram | Mostly | Yes | Requirement boxes stay #ececff throughout |
| journey | Mostly | Yes | Face stroke, mouth, section and task bar strokes, actor rings |
| timeline | Mostly | Yes | Shares the journey actor-ring rule |
| architecture-beta | Mostly | Yes | The icon plate is a hardcoded blue inline style |
| pie | No | Yes | All twelve slices and the legend keep Mermaid’s ramp |
| quadrantChart | No | Yes | The four grounds ignore quadrant1Fill–quadrant4Fill |
| xychart-beta | No | Yes | Its config carries no color keys at all in 11.17.2 |
| radar-beta | No | Yes | Curves, legend boxes and graticule all paint from constants |
| treemap-beta | No | Yes | Leaves and the section frame paint from constants |
| venn-beta | No | Yes | Set fills and labels are inline-styled |
| cynefin-beta | No | Yes | Five pastel domains, matched on the literal |
| C4Context | Connectors only | Partly | Shapes are unreachable — see below |
| sankey-beta | No | Left alone | Paints from d3’s Tableau10 — deliberately kept; see below |
| railroad / ABNF | Via the railroad config block only | No | Its sanitizer rejects every var() |
Three exceptions, documented so nobody re-debugs them:
- C4Context — its shapes and their labels carry inline
fill:… !important, the top of the cascade. Neither file reaches them, and itsc4config block is accepted and then ignored. Style C4 from the diagram source withUpdateElementStyle. Its relationship connectors and the relationship label are plain-reachable and leed.ai’s stylesheet does reach them, so do not conclude from a partly-brand C4 diagram that the problem is fixed. - sankey-beta — the one type deliberately left on Mermaid’s own colors. Its d3 Tableau10 scheme is a ten-hue qualitative scale, and a sankey is read by following a flow through nodes that have to stay told apart; a warm brand ramp separates well at three series and progressively worse as a real diagram grows. Color is carrying information here in a way it is not in the types above.
tailwind/docs/mermaid.cssrecords the full reasoning, including the selector trap that makes a naive fix repaint unrelated flowcharts. - kanban — looks unthemed and is not. Its column tints are Mermaid’s own color maths run over your accent, so they are brand colors that are not literal tokens. An audit that diffs rendered colors against your token list will keep flagging it. Leave it.
Where the rules have to live, and why both files exist
The lavender #ececff and its #9370db partner are real and still present at 11.17.2, in stateDiagram-v2’s [*] markers, erDiagram’s attribute row bands and requirementDiagram’s boxes. What is not true — and was asserted here until it was measured — is that they are unreachable, or that a diagram has to be rewritten as a flowchart to be themeable. They are matched on the literal in tailwind/docs/mermaid.css and they go brand.
Two rules of thumb when you add to either file:
- If Mermaid passes your value straight into the SVG, it belongs in the palette JSON, where light and dark stay one definition.
- If Mermaid computes with it, or writes the color into its own
#mermaid-Nblock or an inline style, it belongs in the CSS — with!importantonly in that second case, which no plain declaration outranks.
Selectors in that file name two containers, pre.mermaid and .zoomable-fullscreen-svg. Click-to-zoom clones the live SVG into the second one, outside pre.mermaid, so a rule scoped only to the first is absent from the one view large enough to study the diagram.
Every type, rendered
The table above claims every type comes out brand once both files are in place. This is the proof, and it is deliberately not a screenshot: these render in your scheme, on the page making the claim. It is also the page to open after a Mermaid upgrade or a palette change — one screen, every type, both schemes.
Each example is minimal on purpose and is copy-pasteable into a Leed page as-is.
Flowchart
flowchart LR
A([Edit]) --> B{Validate}
B -->|ok| C[Commit]
B -->|fails| D[Fix]
subgraph repo [Your repository]
C --> E[(Push)]
end
Sequence
sequenceDiagram autonumber actor A as Author participant L as Leed A->>L: publish page activate L L-->>A: deployment queued deactivate L Note over L: the build runs
Class
classDiagram
class Page {
+String pageId
+publish()
}
class Revision {
+String path
}
Page "1" --> "*" Revision : has
State
stateDiagram-v2 [*] --> Draft Draft --> Published: publish Published --> Draft: edit Published --> [*]
Entity relationship
erDiagram
PAGE ||--o{ REVISION : has
PAGE {
string pageId
string slug
}
Requirement
requirementDiagram
requirement contrast {
id: 1
text: "chart steps clear 3:1"
risk: medium
verifymethod: test
}
element ramp {
type: palette
}
ramp - satisfies -> contrast
User journey
journey
title Theming a diagram
section Palette
Write the JSON: 5: Developer
Freeze the literals: 3: Developer
section Proof
Render both schemes: 5: Developer
Gantt
gantt
title A styling pass
dateFormat YYYY-MM-DD
section Work
Palette :a1, 2026-09-01, 4d
Stylesheet:after a1, 6d
Pie
pie title Where the build time goes "Minify" : 44 "Render" : 31 "Tailwind" : 25
Quadrant
quadrantChart title Effort against value x-axis Low effort --> High effort y-axis Low value --> High value quadrant-1 Do now quadrant-2 Plan quadrant-3 Drop quadrant-4 Maybe Palette: [0.3, 0.8] Stylesheet: [0.7, 0.4]
Git graph
gitGraph commit branch theming commit checkout main merge theming commit
Mindmap
mindmap
root((Styling))
Palette
Tokens
Literals
Stylesheet
Cascade
Timeline
timeline title A theming pass Day 1 : Tokens Day 2 : Palette : Stylesheet Day 3 : Review
Sankey
sankey-beta Pages,Built,80 Pages,Skipped,20 Built,Deployed,80
XY chart
xychart-beta title "Builds per day" x-axis [mon, tue, wed, thu, fri] y-axis "Builds" 0 --> 100 bar [30, 45, 60, 50, 80] line [30, 45, 60, 50, 80]
Block
block-beta columns 3 Browser space Worker space:3 D1:3
Packet
packet-beta 0-15: "Page id" 16-31: "Revision" 32-63: "Body digest"
Architecture
architecture-beta group site(cloud)[Your site] service worker(server)[Worker] in site service db(database)[D1] in site worker:R --> L:db
Kanban
kanban
Todo
[Write the palette]
Doing
[Theme the diagrams]
Done
[Pick the tokens]
C4 context
UpdateElementStyle is not decoration here — it is the only thing that colors a C4 diagram, for the reason given above.
C4Context title Publishing Person(author, "Author") System(cms, "Leed CMS") Rel(author, cms, "publishes") UpdateElementStyle(author, $bgColor="#e60076", $borderColor="#c6005c", $fontColor="#ffffff") UpdateElementStyle(cms, $bgColor="#8a8688", $borderColor="#686466", $fontColor="#ffffff")
Swimlane
swimlane-beta Author CMS Build
Radar
Four curves on purpose: one series would not show whether the ramp is being read at all.
radar-beta
title Theming coverage
axis pal["Palette"], css["Stylesheet"], dark["Dark"], zoom["Zoom"], docs["Docs"]
curve now["Today"]{4, 3, 5, 2, 3}
curve next["Next"]{2, 5, 3, 4, 1}
curve gaps["Gaps"]{1, 2, 2, 5, 4}
curve later["Later"]{5, 1, 4, 3, 2}
max 5
Treemap
treemap-beta "Your repository" "Templates": 40 "Stylesheets": 35 "Static": 25
Tree view
treeView-beta
raw-content/
src/
docs/
start-here/
what-is-leed.md :::highlight ## the set's starting page
_includes/
docs-header.hbs ## eject to customise it
tailwind/
docs/
Venn
venn-beta title Where a colour can live set palette set stylesheet
Ishikawa
ishikawa-beta
title Diagram renders unstyled
category Palette
cause theme base not set
Cynefin
All four domains, so the five regions — four plus the confusion wedge at the center — are on screen at once.
cynefin-beta title Choosing a fix clear "Set a theme variable" complicated "Write a scoped rule" complex "Re-derive the ramp" chaotic "Every diagram is raw source"
What genuinely still needs a rule
Kept separate from the stale list above so the two do not blur, and framed as three reusable classes of fix rather than as a list of one site’s rules. When you meet a color the palette does not reach, work out which class it is and the weight follows.
Class 1 — a presentation attribute. A plain declaration wins. The journey and timeline actor rings are stroked #000 as an attribute, with no theme variable behind them:
pre.mermaid svg circle[class^="actor-"] {
stroke: var(--fg-muted);
}Verdict: measured, a plain declaration wins. Note the attribute-shaped selector used for the section and task bars — rect.task[stroke="#666"] targets exactly the literal being replaced, so if a future release themes it the rule stops matching instead of silently overriding a value the palette had started supplying correctly.
Class 2 — an inline style. !important is required. The architecture-beta icon pack wraps every glyph in a hardcoded blue plate written as an inline style:
pre.mermaid svg[aria-roledescription="architecture"] rect[style*="#087ebf"] {
fill: var(--primary) !important;
}Verdict: measured, a plain declaration loses. The attribute selector matches the raw style string, so #087ebf works even though the parsed value reads back as rgb(8, 126, 191).
Class 3 — a color named inside Mermaid’s own id-scoped block. !important is required and nothing else will do. The journey face ring and mouth sit beside a themeable fill in the same Mermaid rule:
pre.mermaid svg circle.face { stroke: var(--fg-muted) !important; }
pre.mermaid svg .mouth { stroke: var(--fg-muted) !important; }Verdict: measured, a plain declaration loses. The face fill is not here at all, because faceColor in the palette moves it — the same diagram, one color, two different mechanisms sitting on adjacent lines.
Run the same attribution check on your own diagram rather than copying these rules: inspect the element, find where the value is actually written, and pick the weight from the ranked table.
Two traps documented but deliberately not fixed
A rule you can see and a comment explaining why one is absent are both cheaper than a rediscovery.
The sequence-diagram box frame. Mermaid still hardcodes stroke="rgb(0,0,0, 0.5)" as a presentation attribute on the frame that box … end draws. The obvious-looking rule for it is inert, because 11.17.2 also emits an id-scoped rect.rect { stroke: nodeBorder }, which outranks both the attribute and anything a stylesheet can write. leed.ai used to ship that rule; it was measured to change nothing, and had it won it would have been a downgrade — replacing the brand accent with a flat border color. It was removed with a comment recording why. This is the sharpest illustration of the ranked cascade on this page.
The composite-state alternating band. A real literal — #e0e0e0 — inside Mermaid’s own block, which no theme variable reaches, whose fix needs !important. It is left unwritten because no diagram in this documentation set uses composite states. Document what ships, not what might.
Clicking a diagram
Every diagram is click-to-zoom, and the viewer is why a diagram may carry more detail than fits the reading column. Because an SVG cannot go through an <img> — a data: URI would put it in a separate document where the page’s custom properties no longer resolve — the node is cloned into the page, and max-width: none !important defeats the inline width Mermaid sized it to for its column.
Two consequences, both requirements rather than options.
var() survives the clone; selectors do not. The viewer clones the diagram’s <pre> as a carrier, id stripped and class kept, so a rule scoped to pre.mermaid svg matches in both places and no second selector is needed. If a zoomed diagram loses its colors while the page copy keeps them, the installed site-management predates that carrier — check with grep -c "cloneNode(false)" dist/static/javascript/zoomable.js, where a 0 means it does.
The stage paints var(--component-bg-color, #ffffff) over a 90%-black backdrop. Without that token a light diagram lands dark-on-dark and a dark one lands on hard white — a failure that appears only after a click, and only in one scheme. The resolution chain is:
--component-bg-color: var(--docs-bg-color, var(--site-bg-color, var(--default-bg-color)));Both of the first two legs are load-bearing. --docs-bg-color carries documentation pages; --site-bg-color carries a diagram in a blog or resource page, where no docs theme class is present. The last rung is slate-950, a blue-black that will not match a neutral page. Set one of the first two in both schemes, and verify by zooming a diagram in dark mode. Both tokens are cataloged on the site token contract, and the reader-facing behavior of the viewer is on the documentation reading experience.
Keeping a palette honest
The JSON cannot carry comments — it is parsed with a strict JSON parser, so a // line is a build warning and an unthemed site. Put the derivation table for every frozen literal in the companion stylesheet, which does take comments, and treat the two files as one change.
Nothing warns you when a token moves. Every literal in the palette is a hand-copied resolution of a CSS custom property, and there is no mechanism by which the JSON could re-read it. Write the token name beside every hex.
Open this page in both schemes after every Mermaid upgrade. An upgrade both fixes and breaks. 11.17.2 routed class and state node fill through the palette — a genuine fix — and in the same release started theming the sequence box frame, which silently made a shipped rule inert. Mermaid is pinned by the site builder rather than by your repository, so an upgrade arrives with a builder release rather than with a change of yours.
Adopting your own scheme
The checklist that makes all of this transferable, in order:
- Pick the site-level tokens the palette will name, and check each one is declared on bare
:rootin both schemes. - Copy the eleven literals and freeze them per scheme, resolving each from the token it stands for.
- Keep
fontFamilyas a literal stack, and keepnoteBorderColorandsequenceNumberColorset. - Derive a categorical ramp — its own problem, with its own arithmetic, in diagram color scales.
- Set the nested objects for the types you actually use.
- Run the sentinel test on anything still wrong, and put the fix in whichever file the test names.
- Open this page in both schemes.
Writing the diagram itself — the fence, the supported types and how it renders — is diagrams. A diagram follows the reader’s system setting for the same reason everything else does, and by the same mechanism, described on dark mode. The palette sits beside the extra highlight.js languages in the same directory and for the same reason, which is cataloged with every other injection point in adding your own CSS and JS. And the error and success colors your palette hands to gantt come from the same semantic tokens the alert registers use, on theming alerts.