Adding support for dark and light images to Hugo's figure shortcode

Introduction

Happy new year everyone! This will be a short blog post to start my 2023 blogging year!

Supporting dark and light themes on websites is very important to me. My website supports it and I want my blog posts to support it too. Last year in September I was writing my blog post called using RDP to control your work laptop with your own setup and I ran into a little problem.

This website is built with Hugo which provides some wonderful predefined shortcodes that let you render “complicated” HTML in markdown while also making it reusable. Whenever I want to render an image, I use the {{< figure >}} shortcode that allows you to render an image with a caption:

{{< figure 
    src="/images/blog/hugo-figure-dynamic/example-image-unsplash-yZaUaEE8psQ.webp" 
    attr="Image was found on Unsplash" 
    attrlink="https://unsplash.com/photos/yZaUaEE8psQ" 
    alt="Example image of the stars at night to showcase Hugo's figure shortcode" 
    caption="This is a caption."
>}}
Example image of the stars at night to showcase Hugo's figure shortcode

This is a caption. Image was found on Unsplash

The problem with the figure shortcode

The problem I had with this approach is that you can only configure a single image source. The RDP blog post I mentioned above contains lots of images portraying several Windows applications, which can either be very bright or dark depending on the user’s theme.

If I wanted to use the figure shortcode I would need to choose between using a dark or light themed image, which is not ideal. Users would be blinded if I would render a bright white image on their dark themed screen, and it would be a bit ugly to render a dark image on a white screen.

A character from &lsquo;The SpongeBob SquarePants Movie&rsquo; screaming &lsquo;My eyes&rsquo; at a bright bald head, or, in this case, a very bright image.

A character from ‘The SpongeBob SquarePants Movie’ screaming ‘My eyes’ at a bright bald head, or, in this case, a very bright image.

The ideal solution would be to render a dark image when the user is using a dark theme, and a light image when the user is using a light theme! So that is what I set out to do. And luckily this can be done using only HTML, no JavaScript needed!

Creating my own shortcode

The figure shortcode contains 90% of the features that I want, so I thought it best to extend the shortcode with this new “dynamic” image feature. As far as I know, you can’t really extend an existing shortcode in Hugo. So I created my own shortcode called figure-dynamic and based it on the figure shortcode from Hugo.

I started with copying the source code of the figure shortcode which can be found here:

As you can see, this uses the <img> element, as expected. We’ll have to change this so we can use a different picture depending on the user’s theme preference.

Using the HTML picture element and prefers-colors-scheme media feature

Luckily, we can use the <picture> element to specify different images based on certain conditions. These conditions include things like device capabilities, device orientation, browser preferences, etc.. This way you can present the best image to the user! You can read more about it here.

The <picture> element supports the media attribute which allows us to use the excellent prefers-color-scheme media feature. This feature tells us if the user prefers a light or dark theme. You can read more about this feature here.

Now we just need to change the src parameter of the figure-dynamic shortcode by changing it into 2 parameters: light-src and dark-src.

Final result

The final result doesn’t look much different than the figure shortcode, except that we now use light-src and dark-src to specify different images to show depending on the browser theme:

{{<figure-dynamic
    dark-src="/images/blog/hugo-figure-dynamic/dark-theme-unsplash-3ym-ev0Pe58.webp" 
    light-src="/images/blog/hugo-figure-dynamic/light-theme-unsplash-TSgwbumanuE.webp"  
    alt="An image of the moon if you use a dark theme. If you use a light theme, the image will be of the sun with some clouds."
    caption="If you see Earth's moon, you prefer a dark theme. If you see Earth's sun, you prefer a light theme.</br>"
    attr="Attribution links can be found at the bottom of the post"
>}}
An image of the moon if you use a dark theme. If you use a light theme, the image will be of the sun with some clouds.

If you see Earth’s moon, you prefer a dark theme. If you see Earth’s sun, you prefer a light theme.
Attribution links can be found at the bottom of the post

Source code

To use this in your own project, do the following:

  1. Ensure the following path exists in your Hugo project: layouts/shortcodes/
  2. Create a new file in that folder for the shortcode. Mine is called figure-dynamic.html
  3. Put the following code in it:
    <! -- Based on Hugo figure shortcode: https://github.com/gohugoio/hugo/blob/e402d91ee199afcace8ae75da6c3587bb8089ace/tpl/tplimpl/embedded/templates/shortcodes/figure.html -->
    <figure{{ with .Get "class" }} class="{{ . }}"{{ end }}>
        {{- if .Get "link" -}}
            <a href="{{ .Get "link" }}"{{ with .Get "target" }} target="{{ . }}"{{ end }}{{ with .Get "rel" }} rel="{{ . }}"{{ end }}>
        {{- end -}}
        <picture>
            <source srcset="{{ .Get "dark-src" }}"
                {{- if or (.Get "alt") (.Get "caption") }}
                alt="{{ with .Get "alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify| plainify }}{{ end }}"
                {{- end -}}
                {{- with .Get "width" }} width="{{ . }}"{{ end -}}
                {{- with .Get "height" }} height="{{ . }}"{{ end -}}
                media="(prefers-color-scheme: dark)">
    
            <img src="{{ .Get "light-src" }}"
                {{- if or (.Get "alt") (.Get "caption") }}
                alt="{{ with .Get "alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify| plainify }}{{ end }}"
                {{- end -}}
                {{- with .Get "width" }} width="{{ . }}"{{ end -}}
                {{- with .Get "height" }} height="{{ . }}"{{ end -}}>
        </picture>
        {{- if .Get "link" }}</a>{{ end -}}
        {{- if or (or (.Get "title") (.Get "caption")) (.Get "attr") -}}
            <figcaption>
                {{ with (.Get "title") -}}
                    <h4>{{ . }}</h4>
                {{- end -}}
                {{- if or (.Get "caption") (.Get "attr") -}}<p>
                    {{- .Get "caption" | markdownify -}}
                    {{- with .Get "attrlink" }}
                        <a href="{{ . }}">
                    {{- end -}}
                    {{- .Get "attr" | markdownify -}}
                    {{- if .Get "attrlink" }}</a>{{ end }}</p>
                {{- end }}
            </figcaption>
        {{- end }}
    </figure>
    
  4. To use this, take a look at the Final result section of this blog post and at the official figure shortcode documentation for all other available properties.
Warning
Be aware of Hugo’s license! At the time of writing, Hugo is licensed with the Apache License 2.0 license. The code in figure-dynamic contains a modified copy of Hugo’s figure shortcode, so your project will also need to honor Hugo’s license if if you decide to use figure-dynamic.

Adding dark/light theme support for non-picture attributes

Sadly I haven’t figured out a shortcode-isolated and HTML-only way to support light/dark values for the other attributes like title, caption, attr, attrlink, etc.. Which is why the attribution links are in the bottom of this blog post.

It’s easy to introduce a hacky solution for these attributes by creating a media query in a <style> block or CSS file, but these are not optimal. Using a <style> block in the shortcode would create duplicate code when you use multiple figure shortcodes, and putting the styling in a CSS file would force a developer that wants to use this figure-dynamic shortcode to define some hyperspecific CSS somewhere. I don’t want to force a developer to have to do this, so I will leave this as an exercise to the reader. If you are interested in this, I believe a pragmatic solution would be:

  1. Create the following CSS classes:
    @media (prefers-color-scheme: dark) {
      .figure-dynamic-light-theme {
        display: none;
      }
    }
    
    @media (prefers-color-scheme: light) {
      .figure-dynamic-dark-theme {
        display: none;
      }
    }
    
  2. Modify the figure-dynamic shortcode by using the CSS classes. This example adds support for a dark-title and light-title. You can still specify title if the title can remain the same for both images:
    {{ if or (.Get "dark-title") (.Get "light-title") }}
        <h4 class="figure-dynamic-dark-theme">{{ .Get "dark-title" }}</h4>
        <h4 class="figure-dynamic-light-theme">{{ .Get "light-title" }}</h4>
    {{ else }}
    {{ with (.Get "title") -}}
        <h4>{{ . }}</h4>
    {{- end -}}
    {{ end }}
    
  3. You still need to modify the if statement above the <figcaption> to check for the existence of the new dark/light-* properties as well.
  4. Continue adding support for other properties like dark/light-attr, dark/light-attrlink, dark/light-caption, etc.

Adding support for attributes like dark/light-alt, dark/light-width, dark/light-height is relatively simple because they are only used in the <picture> element which is aware of the theme preference. I have not created these properties because I don’t need them. Adding these properties is an exercise for the reader.

Finishing up

I’d like to thank Sam Goodgame on Unsplash for the picture of the moon, and CHUTTERSNAP on Unsplash for the picture of the sun!

If you have any questions or know of any improvements, let me know in the comments below!