Inlining SVGs for Dark Mode

Inlining SVGs for Dark Mode

I will here indulge in the traditional practice of using my blog to talk about how I’m using my blog. This page is built with the Hugo static site generator. I recently updated it to use the latest version of the beautifulhugo theme, which unbeknownst to me included a dark mode colorscheme. Recent browsers use the prefers-color-scheme option to automatically choose light or dark mode CSS styles, if the website supports it. And my website did support it, not that I knew it until people started commenting that my syntax-highlighted code blocks were unreadable! I figured out how to toggle light/dark mode in Firefox (ctrl+shift+I to open the inspector pane then click the sun/moon icons), perused my website, and found an even greater problem: my treasured vector diagrams that I put so much time & effort into were completely invisible against a dark background! Here’s a quick post about supporting dark mode on my blog by inlining SVGs and setting their color with the currentColor CSS variable.

My diagrams!

On some of my posts I spent an alarming amount of time creating beautiful vector diagrams in TikZ. You can see them on this post and this post. They were totally invisible on dark mode! My precious diagrams! Unacceptable.

Cabbage Man character from The Last Airbender with diagrams standing in for the cabbages

I’ll demonstrate the problem here. Try toggling light & dark mode while looking at this diagram, which is embedded with the <img src=... /> HTML tag:

This diagram is a SVG (Scalable Vector Graphics) image, the format of which is actually human-readable! If you open this SVG file in an editor you’ll see a large XML document that looks like:

<?xml version='1.0' encoding='UTF-8'?>
<!-- This file was generated by dvisvgm 2.8.2 -->
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='auto' height='auto' viewBox='0 -169.028 193.633 169.028' style="stroke:currentColor; fill:currentColor; stroke-width:0" role="img">
<title>A state machine where each transition is labeled with either H or T for heads or tails. The state machine fans out like a four-level full binary tree from the start state, with the exception of the paths only flipping heads or only flipping tails. Starting from state 0 H goes to state 1, then H goes to state 3; however, from state 3 only T goes to one of the six termination states while H goes back to state 1 to form an infinite loop. There is an analogous loop on the T half of the tree.</title>
<g id='page1'>
<g transform='matrix(1 0 0 -1 0 0)'>
<path d='M65.375 84.515625C65.375 91.39063 59.80078 96.9688 52.921875 96.9688C46.04687 96.9688 40.4687 91.39063 40.4687 84.515625C40.4687 77.63672 46.04687 72.0586 52.921875 72.0586C59.80078 72.0586 65.375 77.63672 65.375 84.515625Z' stroke='currentColor' fill='none' stroke-width='.3985' stroke-miterlimit='10'/>
<path d='M52.24766 86.84263C51.96891 86.83263 51.76953 86.61325 51.76953 86.39419C51.76953 86.25482 51.85922 86.10544 52.0786 86.10544C52.29766 86.10544 52.53672 86.27482 52.53672 86.66325C52.53672 87.11138 52.10828 87.51982 51.35141 87.51982C50.03641 87.51982 49.66797 86.50388 49.66797 86.06544C49.66797 85.28857 50.40485 85.13919 50.69391 85.0795C51.21172 84.97982 51.72985 84.87013 51.72985 84.32232C51.72985 84.063254 51.50078 83.226691 50.30547 83.226691C50.16578 83.226691 49.39891 83.226691 49.169846 83.754504C49.54828 83.704816 49.79735 84.003566 49.79735 84.28232C49.79735 84.51169 49.63797 84.63107 49.42891 84.63107C49.169846 84.63107 48.871096 84.422 48.871096 83.973566C48.871096 83.405754 49.4386 83.007316 50.29547 83.007316C51.90922 83.007316 52.29766 84.21263 52.29766 84.66107C52.29766 85.0195 52.10828 85.26857 51.98891 85.38825C51.71985 85.667 51.4311 85.717 50.99266 85.80638C50.63422 85.88638 50.23578 85.95607 50.23578 86.40419C50.23578 86.69294 50.47485 87.30075 51.35141 87.30075C51.60047 87.30075 52.09828 87.23107 52.24766 86.84263Z'/>
...

Every item in the SVG (letters, arrows, circles, etc.) is defined by a <path> item plotting out a series of points along which a curve is interpolated. Each of these <path> items has stroke and fill attributes controlling its color. The key thing to note here is SVG files are also valid HTML, so can be inlined in a larger HTML document. Then, if you replace the hardcoded black #000 color value in the stroke and fill attributes with the currentColor keyword, the paths will take on the correct color to be displayed against the background! Here’s the exact same SVG file, but this time it’s inlined; see how it changes color as you toggle light & dark mode!

A state machine where each transition is labeled with either H or T for heads or tails. The state machine fans out like a four-level full binary tree from the start state, with the exception of the paths only flipping heads or only flipping tails. Starting from state 0 H goes to state 1, then H goes to state 3; however, from state 3 only T goes to one of the six termination states while H goes back to state 1 to form an infinite loop. There is an analogous loop on the T half of the tree.

If you view the page source you’ll indeed see the full SVG present in the HTML. The currentColor keyword doesn’t work on embedded SVGs, only inlined SVGs. If you want to get even fancier you can define other color values (like the red in the diagram above) as CSS variables whose value depends on the light/dark mode setting, but I haven’t gone there yet.

In Hugo I was easily able to inline the SVG using the following custom shortcode, in layouts/shortcodes/inline-vector-figure.html:

{{ $svgFile := (path.Join (path.Dir .Page.File.Path) (.Get 0)) }}
{{ os.ReadFile $svgFile | safeHTML }}

Then in this post I use the shortcode by specifying the relative path to the SVG:

{{< inline-vector-figure "../2020-09-11-probabilistic-distsys/knuth-yao.svg" >}}

There were a few other things to figure out:

  • Default Color: These diagrams were generated with TikZ, which outputs text as paths (although SVG does have a text element) that for some reason do not have stroke or fill attributes. I could have painstakingly gone through my diagrams and added these attributes to all the relevant paths, but SVG also supports setting default values for these in the top-level <svg> tag with the attribute style="stroke:currentColor; fill:currentColor; stroke-width:0".
  • Scaling: there’s an incredibly detailed post about scaling SVGs here, but I just changed the width and height attribute values in the top-level <svg> tag to auto and it seems to work: the displayed SVG scales to the width of the text box. Possibly this only works on newer browsers, but I’m more concerned with my posts being readable in the future than in the past; that’s why I made vector diagrams in the first place, so they’d stay beautiful as screen resolution increased over time!
  • Accessibility: there is currently no standardized method of adding alt-text to inlined SVGs, so I followed the recommendations in this post and added the alt-text as a <title> element in the SVG as a best effort; you can see it by hovering over the inlined SVG up above. I also added the role="img" attribute to the top-level <svg> tag.

With that I declared victory. Oddly this whole process took me very little time; I can see why people enjoy web development, browsers are amazing! In all other fields of software development nothing just works; there are always tiny details that trip you up and take hours to work around. I suppose that wasn’t the case here because browsers have had an unfathomable quantity of work put into them and are very permissive in what they accept while still generating good-looking documents.

One last thing, Hugo actually has native support for ASCII diagrams so if you’re reading this and thinking about making your own diagrams that could be a simpler option for you:

1 2 3 4 1 2 3 4 1 2 3 4

Syntax Highlighting

The root cause of the unreadable syntax highlighting issue is a bug in the theme I am using. Hugo supports syntax highlighting for a huge list of languages, but neither TLA⁺ nor Lean are among them. My current solution is to switch between using Haskell, Idris, or (oddly) Julia highlighting as good-enough stand-ins for these languages. I could whip up a regex-based highlighting file for TLA⁺ to get it supported by Hugo (it doesn’t seem too tricky, it even has obscure languages like Whiley!) but after spending so much time writing a TLA⁺ tree-sitter grammar it seems a betrayal of that work to regress from grammar-based highlighting to ordinary regexes.

I spent some time looking at what it would take to add tree-sitter highlighting to Hugo. Andrew T. Biehl managed to do it for Jekyll. As far as I can tell I would have to write an extension for goldmark, the markdown parser Hugo uses. This is how ASCII diagrams are implemented. I took a few steps in this direction but the estimated effort seemed to rapidly exceed how much I cared. So, a project for another time!

Conclusion

Thank you to everybody who messaged or commented about the dark mode rendering problems, and especially to lobste.rs users predrag and mk12 for explaining dark mode and how to handle it! Also, if you know a better method for creating beautiful SVG diagrams than TikZ please let me know.

Discussions

Highlighted Comments

  • Users ftio on Hacker News and atocanist on lobste.rs noted it is possible to define a CSS style sheet inside the SVG itself, so it will be responsive even if embedded with the <img> tag instead of inlined.
  • Lobste.rs user Jordan Webb noted a possible pitfall when inlining SVGs with leaky style tags.