Implementing configurable Base16 schemes in Hugo themes

I love the Base16 project which provides a template for color schemes and a decentralized set of tools and data to make customization easier.

I would like to implement them for Hugo sites. While there are existing solutions, it can be tedious to configure them. I’ve always wondered if there is an easier way to use the schemes as-is. It will make color customization very easy in a Hugo theme.

The good thing is I was able to implement it. The following recording demonstrates that fully.

I also ported the feature into Xmin theme. [1]

I like it so much that I also added it in my theme initially.

This is not revolutionary in any way as there are themes that have already done it. Case in point, the Academic theme with their own version.

For this demonstration, it accepts a bunch of Base16 schemes in a specific location (e.g., data/themes) and the themes should appear. To make that possible, we’ll make use of data templates, resource templates, and CSS variables.

For this post, I assume you’re already familiar with Hugo along with some experience modifying themes. As such, I’ll be showing some stuff that will be quickly glazed over. For future references, you also need Hugo v0.74.0 and above.

The bigger picture

Before we head into the code, we need to think and recap the workflow of adding custom Base16 themes.

  • To add a theme, we simply need to place it in a specified location (e.g., data/themes/).

  • The theme would have to build a stylesheet that dynamically make use of the Base16 palette.

  • The theme’s stylesheet would have to built around the Base16 palette.

  • A theme selection interface would be shown for the user to freely select the themes.

  • The theme should have a default theme in case no custom themes has been added.

For the user, they would only have to put the Base16 schemes and the job’s done.

Converting the schemes into CSS

Let’s start with building the CSS created from the schemes. It is pretty easy to map them into CSS which we’ll make use of CSS variables.

Given the following scheme…​

scheme: "Default Dark"
author: "Chris Kempson (http://chriskempson.com)"
base00: "181818"
base01: "282828"
base02: "383838"
base03: "585858"
base04: "b8b8b8"
base05: "d8d8d8"
base06: "e8e8e8"
base07: "f8f8f8"
base08: "ab4642"
base09: "dc9656"
base0A: "f7ca88"
base0B: "a1b56c"
base0C: "86c1b9"
base0D: "7cafc2"
base0E: "ba8baf"
base0F: "a16946"

…​will produce the following CSS.

[data-theme="Default Dark"]:root {
  --base00: #181818;
  --base01: #282828;
  --base02: #383838;
  --base03: #585858;
  --base04: #b8b8b8;
  --base05: #d8d8d8;
  --base06: #e8e8e8;
  --base07: #f8f8f8;
  --base08: #ab4642;
  --base09: #dc9656;
  --base0A: #f7ca88;
  --base0B: #a1b56c;
  --base0C: #86c1b9;
  --base0D: #7cafc2;
  --base0E: #ba8baf;
  --base0F: #a16946; }

As for the template, the following code is just one more concise to do that. It will be stored as an asset template in assets/templates/scheme.css which we can create a resource out of it.

{{- $name := index $ "name" -}}
{{- $scheme := index $ "scheme" -}}
{{- if ne $name "_index" }}[data-theme="{{ $scheme.scheme }}"]{{ end }}:root {
  {{- range $i := seq 0 15 }}
  {{- $hex := upper (printf "%02x" $i) }}
  {{- $key := printf "base%s" $hex }}
    --{{ $key }}: #{{ index $scheme $key }};
  {{- end -}}
}

In this case, we make full use of CSS variables which is supported by ~96% of the major browsers (as of 2020-11-08).

Building the CSS of the site

Now that we have the template, it’s time to make use of it. In one of Hugo asset processing functions, we can combine multiple assets together. While not required, it is better to make it so that the client will make one less request for the stylesheet.

The following block shows an example on how to make use of it. This will vary according how the theme links its CSS files.

{{- $style := resources.Get "css/style.css" }}
{{- $styles := slice $style -}}

{{- $scheme_template := resources.Get "templates/scheme.css" }}
{{- range $name, $scheme := $.Site.Data.themes }}
  {{- $scheme := $scheme_template | resources.ExecuteAsTemplate (printf "css/themes/%s.css" $name) (dict "name" $name "scheme" $scheme) }}
  {{- $styles = $styles | append $scheme }}
{{- end }}

{{- $styles = $styles | resources.Concat "css/index.css" }}
<link rel="stylesheet" href="{{ $styles.Permalink }}" />

Creating the interface for switching themes

Now that the styles are in place, we need to have an interface to switch themes.

In my version, the button will only appear if there’s more than one theme. Furthermore, it will store the selected theme in the local storage.

{{- if gt (len (index $.Site.Data "themes")) 1 }}
<div class="site__theme-btn" aria-label="Theme toggle">
  <svg xmlns="http://www.w3.org/2000/svg" id="color-swatch" viewBox="0 0 20 20" fill="currentColor">
    <path fill-rule="evenodd" d="M4 2a2 2 0 00-2 2v11a3 3 0 106 0V4a2 2 0 00-2-2H4zm1 14a1 1 0 100-2 1 1 0 000 2zm5-1.757l4.9-4.9a2 2 0 000-2.828L13.485 5.1a2 2 0 00-2.828 0L10 5.757v8.486zM16 18H9.071l6-6H16a2 2 0 012 2v2a2 2 0 01-2 2z" clip-rule="evenodd"/>
  </svg>
  <div class="site__theme-dropdown">
    {{- range $filename, $scheme := (index $.Site.Data "themes") }}
    {{- $name := cond (eq $filename "_index") (printf "%s (default)" .scheme) .scheme }}
    <div class="site__theme-item" {{ if ne $filename "_index" }}data-theme="{{ .scheme }}"{{ end }}>{{ $name }}</div>
    {{- end }}
  </div>
</div>

<script defer>
  const themeDropdown = document.querySelector('.site__theme-btn');
  themeDropdown.addEventListener('click', (event) => {
    const { target } = event;
    if (target.classList.contains("site__theme-item")) {
      if (target.dataset.theme) {
        theme = target.dataset.theme;
        window.localStorage.setItem("theme", theme);
        document.documentElement.dataset.theme = theme;
      } else {
        window.localStorage.removeItem("theme");
        delete document.documentElement.dataset.theme;
      }
    }
  });
</script>

<style>
.site__theme-btn svg {
  width: 2em;
  height: 2em;
}

.site__theme-btn {
  background: var(--base00);
  border: var(--border-style);
  position: absolute;
  padding: 0.5em;
  right: 0;
  top: 0;
}

.site__theme-btn:hover svg {
  display: none;
}

.site__theme-dropdown {
  display: none;
  position: relative;
  left: 0;
}

.site__theme-btn:hover .site__theme-dropdown {
  display: unset;
}

.site__theme-dropdown .site__theme-item:hover {
  background: var(--base0C);
  color: var(--base00);
  cursor: pointer;
}
</style>
{{- end }}

We still have yet to make our selected theme persistent. The following snippet will take care of that.

<script>
  let theme = window.localStorage.getItem('theme');
  if (theme) {
    document.documentElement.dataset.theme = theme;
  }
</script>

It should be placed preferably after the main stylesheet was loaded to mitigate against flashes of unstyled content.

Conclusion

With all of the components in place, we can easily customize the colors for our themes. Though, there are some bumps to go through with this approach.

  • You have to modify the entire CSS files to fit with the Base16 color palette.

  • Styling with 16 colors can be hard especially with the aim of consistency so you’ll have to style the theme carefully.

  • Not all of the schemes will look easy on the eyes nor consistent.

  • It could also be a bane to create a palette of 16 colors that looks nice.

  • Multiple themes, while nice-to-have, is not integral to create a branding which is what most authors aim (I assume).

Indeed, this is just a niche feature. However, this feature could be derived into something simpler which is what the Academic theme already has.

Still, I hope this is something that Hugo theme developers will consider. It will make the Hugo ecosystem more colorful.


1. For those who are looking for the code, here’s the source for it. I didn’t put it on a remote Git repo since it is just a small fork, anyways.