Data-Dive

Adding adaptive syntax highlighting to your Hugo blog's dark mode

· mc51

Context

Adding a dark mode to your Hugo blog is a good way to appeal to your readers. Many Hugo themes already implement this functionality (I use paper). But even adding it manually is simple enough. However, if you use code snippets with syntax highlighting in your posts your style / theme will likely only match either light or dark mode. Imagine having a relaxed read on data-dive.com using dark-mode just to have your eyes strained by something like this:

A blog post on dark-mode with an unsuitable light syntax highlighting theme
Figure 1. A blog post on dark-mode with an unsuitable light syntax highlighting theme

Not in my house! I respect your eyesight, so I want it to look like this:

A blog post on dark-mode with a matching dark syntax highlighting theme
Figure 2. A blog post on dark-mode with a matching dark syntax highlighting theme

The solution? We make the syntax highlighting style adaptive. We use a theme suited for light-mode and one for dark-mode. To do so, we only need a bit of JavaScript and some quick edits to the Hugo config.

Instructions

The recent versions of Hugo uses chroma for syntax highlighting. So, first decide on the styles you want to use. I like monokailight for light-mode and onedark for dark-mode. Next, create the corresponding .css files to be used by hugo:

hugo gen chromastyles --style=monokailight > syntax_light.css
hugo gen chromastyles --style=onedark > syntax_dark.css

Move those to your theme’s assets folder (e.g. ./themes/paper/assets/).
Following, make sure that our Hugo theme loads those styles. For this, edit ./themes/paper/layout/partials/head.html (or whichever file is responsible for adding a head part in your theme) and add this:

  {{ $syntax_dark_css := resources.Get "syntax_dark.css" | minify }}
  {{ $syntax_light_css := resources.Get "syntax_light.css" | minify }}

  <link rel="preload stylesheet" as="style" href="{{ $syntax_dark_css.Permalink }}" />
  <link rel="preload stylesheet" as="style" href="{{ $syntax_light_css.Permalink }}" />

This will make the .css files available when building the site with Hugo and allow our page to load them. The order is important: the style in the last referenced file is applied by default, as it overrides the former! To make Hugo use our .css files as chroma styles, we need to explicitly state the following in hugo’s config.toml:

[markup.highlight]
    noClasses          = false

To assure you’re on track, check that your code is now highlighted with the light style. When you switch to dark-mode, nothing will happen just yet.
How to change this? Well, we need to dynamically switch which of the .css files is applied to our code snippets. We can achieve this using some JavaScript.
In ./themes/papers/partials/header.html (or whichever file is responsible for adding the switch between modes in your Hugo theme), we look for the code that toggles modes. In my case, it looks like this:

    const setDark = (isDark) => {
      metaTheme.setAttribute('content', isDark ? '#000' : lightBg);
      htmlClass[isDark ? 'add' : 'remove']('dark');
      localStorage.setItem('dark', isDark);
    };

We add a single function call to this (we’ll write the function next):

    const setDark = (isDark) => {
      metaTheme.setAttribute('content', isDark ? '#000' : lightBg);
      htmlClass[isDark ? 'add' : 'remove']('dark');
      localStorage.setItem('dark', isDark);
      setSyntaxDark(isDark);
    };

Now, let’s define the setSyntaxDark function and an additional helper function:


    function getStyleSheet(file_name) {
      for (var i = 0; i < document.styleSheets.length; i++) {
        var sheet = document.styleSheets[i];
        if (sheet.href.includes(file_name)) {
          return sheet;
        }
      }
    }

    function setSyntaxDark(isDark) {
      sheet_light = getStyleSheet("syntax_light")
      sheet_dark = getStyleSheet("syntax_dark")

      sheet_light.disabled = isDark ? true : false
      sheet_dark.disabled = isDark ? false : true
    }

That’s really the heart of our solution. So, let’s take a second to discuss what’s going on. The getStyleSheet function iterates over all style sheets loaded on the current page. For each sheet, it checks if the url used in href includes the file_name. If it does, it returns a reference to that style sheet. We use this function in setSyntaxDark to get a reference to both our style sheets. There, we also set the disabled property to deactivate one sheet while activating (=applying) the other. This results in toggling between the chroma style used to display the syntax highlighting. So, whenever the dark-mode is activated the syntax_dark.css is the style sheet applied and the other way around.
Subsequently, let Hugo build your site and make sure everything works as expected. If you’re having any issues, the dev-mode console is probably the best place to start debugging.
Finally, deploy your site, grab a drink and rest assured that you vastly improved your readers’ dark-mode experience!