How to theme Mermaid when your palette uses OKLCH
My blog’s whole look is built on OKLCH colors — a modern CSS color space that’s great for perceptually even palettes, defined once in my inline Tailwind v4 @theme config. So when I themed my mermaid diagrams, my first instinct was to reuse those exact OKLCH tokens. It didn’t work: the diagrams came out low-contrast and muddy, ignoring the colors I gave them. This post explains why, and the small fix — mapping my OKLCH tokens to hex before handing them to mermaid.
Why OKLCH breaks mermaid
Mermaid doesn’t just paint the colors you give it. It does color math internally — darkening a fill to derive a border, lightening a background to derive a label color, computing contrast, and so on. For that math it uses a small color library called khroma.
The catch: khroma can’t parse oklch(...) values. When you hand mermaid an OKLCH color, khroma fails to understand it, and mermaid quietly falls back to low-contrast defaults instead of throwing an error. That’s exactly the washed-out look I got — no crash, no warning, just wrong colors. It’s a silent failure, which is the worst kind to debug.
So the rule is simple: give mermaid colors in a format khroma understands. Plain hex (#161616) is the safe choice. I keep OKLCH everywhere else in my CSS; I only translate at the boundary where I talk to mermaid.
The fix: map tokens to hex, pass via themeVariables
Mermaid takes a theme through mermaid.initialize(). You pick a base theme and then override individual colors with themeVariables. I use theme: "base" — the most overridable theme — and then set hex equivalents of my OKLCH terminal palette.
This runs inside the same client script that renders the Mermaid diagrams and only loads mermaid on pages that have one. Here’s the real initialization from my post-enhancement script:
// blog/src/components/PostEnhancements.astro
mermaid.initialize({
startOnLoad: false,
securityLevel: "strict",
theme: "base",
fontFamily:
'"JetBrains Mono Variable", "JetBrains Mono", ui-monospace, monospace',
// NOTE: mermaid's internal color math (khroma) cannot parse oklch(), so it
// silently falls back to low-contrast defaults. These are hex equivalents
// of the blog's OKLCH terminal tokens.
themeVariables: {
darkMode: true,
background: "#0a0a0a",
// Node fills / text
primaryColor: "#161616",
primaryBorderColor: "#6a6a6a",
primaryTextColor: "#e6e6e6",
secondaryColor: "#1f1f1f",
secondaryBorderColor: "#5c5c5c",
secondaryTextColor: "#e6e6e6",
tertiaryColor: "#141414",
tertiaryBorderColor: "#3a3a3a",
tertiaryTextColor: "#e6e6e6",
mainBkg: "#161616",
nodeBorder: "#6a6a6a",
nodeTextColor: "#e6e6e6",
// Edges / arrows
lineColor: "#5aa07a",
// General text + labels
textColor: "#d0d0d0",
titleColor: "#e6e6e6",
edgeLabelBackground: "#0d0d0d",
labelBackground: "#0d0d0d",
labelTextColor: "#d0d0d0",
// Subgraph clusters
clusterBkg: "#111111",
clusterBorder: "#3a3a3a",
// Notes (sequence/flow)
noteBkgColor: "#1f1f1f",
noteBorderColor: "#5c5c5c",
noteTextColor: "#e6e6e6",
fontSize: "14px",
},
})Every value there is a hex string. That’s the deliberate part. My CSS still defines these same colors in OKLCH; the values above are the pre-computed hex equivalents, converted once and pasted in so khroma never sees an oklch() it can’t handle.
What each group of variables controls
themeVariables has a lot of keys, and the names aren’t always obvious. Grouping them by what they paint makes it manageable:
- Canvas —
backgroundis the diagram’s backdrop. I set near-black#0a0a0ato match the terminal aesthetic. - Nodes —
primaryColor/mainBkgare node fills,primaryBorderColor/nodeBorderthe outlines,primaryTextColor/nodeTextColorthe text inside. Thesecondary*andtertiary*sets do the same for mermaid’s second- and third-tier node styles. - Edges —
lineColorcolors the arrows and connectors. This is my one accent: a phosphor-green#5aa07aso the flow lines pop against the gray nodes. - Labels —
textColor,labelTextColor,edgeLabelBackground, andlabelBackgroundhandle text on and around edges, with a dark chip behind edge labels so they stay readable over lines. - Clusters —
clusterBkg/clusterBorderstyle subgraph boxes. - Notes —
noteBkgColor/noteBorderColor/noteTextColorstyle the note boxes in sequence and flow diagrams.
I also set darkMode: true so mermaid’s own derived shades lean dark, and fontFamily to JetBrains Mono so diagram text matches my code blocks.
A quick way to convert OKLCH to hex
You don’t need a build step for this. Any browser devtools console will convert for you, because the browser natively understands OKLCH:
// paste into any devtools console
const el = document.createElement("span")
el.style.color = "oklch(0.2 0 0)"
document.body.appendChild(el)
getComputedStyle(el).color // -> "rgb(...)" you can convert to hexOr use any online OKLCH-to-hex converter. Convert each design token once, drop the hex into themeVariables, and leave a comment noting which OKLCH token it came from so future-you knows they’re meant to stay in sync.
Takeaways
- Mermaid’s color engine (khroma) can’t parse
oklch()and fails silently to low-contrast defaults. - Convert your OKLCH tokens to hex and pass the hex values through
mermaid.initialize({ themeVariables }). - Use
theme: "base"— it’s the most overridable starting point. - Group the variables mentally: canvas, nodes, edges, labels, clusters, notes.
- Keep OKLCH everywhere else in your CSS; only translate at the mermaid boundary, and comment the mapping so it stays in sync.