An upcoming post on this blog will include embedded diagrams. I intended to use Mermaid diagrams with the built-in support of the Coder theme,1 but I quickly realized that it does not support dark mode. Here’s the rabbit hole I went down to fix this.

Dark Theme of Mermaid Link to heading

Mermaid JS is a client-side JavaScript library, that replaces HTML elements containing Mermaid syntax with generated SVG image. Including Mermaid in a website is as simple as adding these four lines, which is essentially what the Coder theme does:2

<script src="https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js"></script>
<script>
    mermaid.initialize({ startOnLoad: true });
</script>

Mermaid also has built-in theming support, including a few predefined themes. To apply the dark theme which fits dark mode, I can pass a configuration variable:

mermaid.initialize({ startOnLoad: true, theme: 'dark' });

This works, but it’s not good enough.

My site allows readers to switch between dark and light mode, selecting a theme automatically based on OS preferences. The current theme is applied by setting the appropriate CSS class on the body element. I could modify the loading script to select the theme based on the current dark or light mode, but there is no way to change the diagram theme dynamically when switching between light and dark mode.

The theme of a Mermaid diagram cannot be changed after rendering.3 Mermaid JS runs once, replacing all diagram sources with rendered SVGs — the original source is lost from the DOM.

Customizing the Mermaid Integration Link to heading

The default Mermaid integration calls the mermaid.run() function upon the DOMContentLoaded event. This function finds all relevant DOM elements and passes them to mermaid.render().4 Although not well-documented, the source code is publicly available.

To implement theme switching, I need to:

  1. Disable automatic rendering.
  2. Find all diagrams.
  3. Save the source code for each diagram.
  4. Call mermaid.render() for each diagram, passing the source code.

If the dark mode changes, I can use the saved source code to re-render all diagrams. This requires a fair bit of plumbing, but ultimately boils down to the following:5

mermaid.initialize({ startOnLoad: false });
function runmermaid() {
    const theme = (document.body.classList.contains('colorscheme-dark') ? 'dark' : 'default')
    mermaid.initialize({ startOnLoad: false, theme: theme});
    const items = document.querySelectorAll('.mermaid');
    let counter = 0;
    for (const item of items) {
        const id = counter++;
        if(item.originalCode === undefined) {
            item.originalCode = item.textContent.trim();
        }
        mermaid.render("mermaid"+id, item.originalCode).then((val) => {
            item.innerHTML = val.svg
        }, (err) => {
            console.log(err);
            // Workaround: move incorrectly placed error messages into their diagram
            item.innerHTML = "";
            item.appendChild(document.getElementById("mermaid" + id));
        });
    }
}
document.addEventListener('DOMContentLoaded', runmermaid);

This code re-renders the diagrams with the matching theme whenever called. The remaining problem is to detect when a theme switch occures.

It is possible to observe the change of a DOM element’s class field via a MutationObserver6, but this approach is complicated and somewhat resource-intensive. Instead, I’ve decided to cut corners and detect the possible causes of a dark mode change.

In my use-case, the possible sources of a theme change are:

  1. The prefers-color-schem media query changing.
  2. The color scheme changer button being clicked.

Both can be listened to using traditional event listeners:

document.getElementById('dark-mode-toggle').addEventListener('click', runmermaid);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', runmermaid);

Fenced Markdown Code Blocks Link to heading

The Coder theme offers Mermaid support via shortcodes but best practices have established using fenced code blocks instead.7 Implementing this is straightforward using the Hugo documentation’s Mermaid diagrams guide. You may wish to maintain backward compatibility with both shortcodes and code blocks. You can use something like this:

{{ if or ( .Store.Get "hasMermaid" ) ( .HasShortcode "mermaid" ) }}
<!-- integartion here -->
{{ end }}

Choosing the code block syntax allows previewing posts without Hugo. For example, Visual Studio Code has the Markdown Preview Mermaid Support extension which works well.

Closing notes Link to heading

I’ll include a Mermaid diagram here, to show how it works. Try changing the theme in the bottom right corner.

graph LR
    A --> B

This diagram was rendered in your browser, with almost exactly the integration you’ve just read about.

I’d personally prefer if all content would be generated on the server-side, including diagrams. However, this does not seem feasible. Since Mermaid lacks external theming support, I’d need to generate multiple versions of each diagram which is suboptimal. Additionally, I’m reluctant to integrate Node.js or JavaScript into the site build process.

This leaves me with a bunch of clean-up and pull requests to be made. Expect to see these improvements upstream sometime soon-ish.


  1. Coder supports Mermaid JS via shortcodes: example, source↩︎

  2. Additional parameters for integrity and crossorigin may be important for security considerations. Refer to Subresource Integrity on MDN ↩︎

  3. Mermaid JS embeds the colors and other theme-related variables in the SVG itself without providing a clean way to change them via CSS externally. ↩︎

  4. Mermaid exposes wrappers around it’s internal API, meaning the functions available for us are not exactly the same as the functions called internally. This isn’t a problem for the specific use-case. ↩︎

  5. This code still cuts corners. ID generation and text content parsing have been simplified. This may cause issues if a website contains indented graphs or dynamically changing content containing graphs. ↩︎

  6. See the MutationObserver in the mdn web docs. ↩︎

  7. See the Hugo documentation for shortcodes, diagrams and code block render hooks. ↩︎