Table Of Contents
Introduction Link to heading
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."
>}}
The problem with the figure shortcode Link to heading
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.
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 Link to heading
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 Link to heading
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 Link to heading
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"
>}}
Source code Link to heading
To use this in your own project, do the following:
- Ensure the following path exists in your Hugo project:
layouts/shortcodes/
- Create a new file in that folder for the shortcode. Mine is called
figure-dynamic.html
- Put the code below in it.
- And finally, 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.
<!-- Based on Hugo figure shortcode: https://github.com/gohugoio/hugo/blobe402d91ee199afcace8ae75da6c3587bb8089ace/tpl/tplimpl/embedded/templates/shortcodes/figurehtml -->
<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 -}}
{{- $attrlink := .Get "attrlink" }}
{{- $attr := .Get "attr" | htmlEscape }}
{{- if $attrlink -}}
{{- printf "[%s](%s)" $attr $attrlink | markdownify -}}
{{- else -}}
{{- $attr | markdownify -}}
{{- end }}
{{- end }}
</figcaption>
{{- end }}
</figure>
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 Link to heading
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:
- 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; } }
- Modify the
figure-dynamic
shortcode by using the CSS classes. This example adds support for adark-title
andlight-title
. You can still specifytitle
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 }}
- You still need to modify the if statement above the
<figcaption>
to check for the existence of the newdark/light-*
properties as well. - 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 Link to heading
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!