CSS Darkmode in SVGs


Introduction

Back in 2010 I was building websites from my bedroom to earn some pocket money. The iPhone 4 had just been released and I had been following the keynote excitedly. A key feature was the Retina display with four times the pixel density of previous iPhones.

In those days websites exclusively used raster images, which when scaled automatically on the new iPhone, would look very pixelated. Image-set was not yet a thing, but there were other workarounds such as media queries with the -webkit-device-pixel-ratio property. This also meant however, that images had to be exported in various resolutions.

But of course there was a better way. Vector images don’t care about pixel density and are incredibly lightweight, as long as the shapes are relatively simple — such as in most logos. I remember playing around with logos in Illustrator CS5 and exporting them as SVGs, manually trying to fix the viewbox and using them in my projects.

Basic SVG

Let’s start with an example. This is a very simple SVG which contains a circle, a rect and a path element as well as a text for the company name.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
  <g>
    <circle cx="100" cy="80" r="80" fill="#0a0a58" />
    <rect width="100" height="100" x="50" y="30" fill="white" />
    <path
      d="M50 142 L100 0 L167 123 C107 60 90 102  50 142  Z"
      fill="#ce2715"
    />
    <text fill="black" x="50%" y="190" text-anchor="middle" font-size="24">
      ACME corp.
    </text>
  </g>
</svg>

For the circle the cxand cy describe the position of the centre, and r the radius. The rect has a width and height and x and y describe the top left corner.

The path is a bit more complex and uses the d attribute to describe the path. The M command moves the cursor to the given coordinates, L draws a line to the given coordinates and C draws a cubic bezier curve, where the first two coordinates describe the position of the handles. The Z command closes the path.

  1. M50 142 Start the path, move to the coordinates 50, 130
  2. L100 00 Draw a straight line to the coordinates 100, 0
  3. L167 123 Draw a straight line to the coordinates 167, 123
  4. C107 60 90 102 50 142 Draw a cubic bezier curve to the coordinates 50, 142. The handles for the curve are located at 107, 60 and 90, 102 respectively.

We also add the text element for the company name. Usually to ensure proper rendering and compatibility the SVG would only contain the outlines of the text, but for this example we’ll keep it simple.

a simple svg logo

There are a lot more topics to cover to create basic SVGs, but we want to focus on the ones that enable dark and light mode. Mozilla has an amazing guide that covers many other basic SVG topics.

Adding Classes

SVG supports styling with CSS, although there are some limitations. Just like in html, classes can provide resuability. Our logo is very simple, so extracting the styles is not strictly necessary — however, for the sake of this post we will do so.

We just added a style element and classes. Then we extract all the in-line styling from the graphical elements to their respective classes and add the class attribute to the elements.

If you embed the SVG with an <img src=".."> tag, you will need to add the styles directly to the SVG. If you add the <svg> in-line into your html, you will be able to use your already imported stylesheets.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
  <style>
    .circle {
      fill: #0a0a58;
    }
    .square {
      fill: #ffffff;
    }
    .arrow {
      fill: #ce2715;
    }
    .text {
      fill: #0a0a58;
      text-anchor: middle;
      font-size: 24px;
    }
  </style>

  <g>
    <circle class="circle" cx="100" cy="80" r="80" />
    <rect class="square" width="100" height="100" x="50" y="30" />
    <path class="arrow" d="M50 142 L100 0 L167 123 C107 60 90 102  50 142  Z" />
    <text class="text" x="50%" y="190">ACME corp.</text>
  </g>
</svg>

Adding Fonts

Our logo looks decent and our styles are in place, but we still lack some of the corporate identity of ACME. We need to add the ritght font.

In most cases you wouldn’t import the whole font into a logo in order to show the company name, but use outlines instead. For this post we will break the rules a little.

The @fontface at-rule in CSS is also available in SVGs, however there are some limitations…

User-generated content is often used as an attack-vector for web-applications, which e.g. has been shown again with the recent WebP vulnerability. The rendering of external images using <img src=".."> blocks requests to external ressources, as this could for instance be used by an attacker to track whoever this picture is shown to. For in-line SVGs, which are part of the DOM or for SVGs specifically rendered using <object> this restriction does not apply.

For our external SVG we therefore need to embed the font. We can make use of the url() CSS funtion and add the base64 encoded WOFF2 file. For encoding it to base64 we can use the following command:

base64 -i font.woff2

This prints the whole font file in base64, which we can then use in the url(). Make sure to add data:application/font-woff2;base64, in front of the base64, to tell the browser to interpret the URL as a URI in base64. We added the following to the styles:

@font-face {
  font-family: "Gloock";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("data:application/font-woff2;base64,d09GMgABAAA[...]") format("woff2");
}
.text {
  font-family: "Gloock";
}

Great. Now we have a nice font added and our Logo is starting to look fancy.

the logo with a new font

Adding CSS Variables

Before we can finally get to implementing darkmode, we will give our styles even more structure by adding CSS variables. CSS variables make it super easy to define themes which can be used throughout your website — or in this case your SVG.

The concept is simple set a variable --main-bg-color: brown; and use it background-color: var(--main-bg-color); throughout your styles. This makes updating the colors for the darkmode a lot easier and readable in the next step.

:root {
  --primary-color: #0a0a58;
  --secondary-color: #ffffff;
  --accent-color: #ce2715;

  --text-color: var(--primary-color);
  --font-family: "Gloock";
  --font-size: 24px;
}

.circle {
  fill: var(--primary-color);
}

.square {
  fill: var(--secondary-color);
}

.arrow {
  fill: var(--accent-color);
}

.text {
  fill: var(--text-color);
  text-anchor: middle;
  font-size: 24px;
  font-family: "Gloock";
}

Adding Darkmode

Native Darkmode (prefers-color-scheme)

Let’s finally add darkmode to our SVG. All modern browsers1 support the prefers-color-scheme CSS at-rule. We can therefore safely use it to tell whether the user’s device is currently using dark mode or not. This is also supported in SVGs, so we could just add it there. However, more and more websites also have dark mode toggles with which the users can override the prefernce set through their browser. These are usually realised by adding or removing classes globally. This however does not apply to SVGs rendered with an <img> tag, but only those inserted into the DOM directly or with <object>.

For the basic darkmode with prefers-color-scheme we need to add the following to our CSS:

@media (prefers-color-scheme: dark) {
  :root {
    --primary-color: #1b1bdd;
    --secondary-color: #bebebe;
    --accent-color: #b9200f;
    --text-color: var(--secondary-color);
  }
}

The @media query tells the browser that the following rules should only apply when the condition prefers-color-scheme: dark is met. Inside we just override the definition of some of the variables we have defined earlier.

We can of course also use variables here to change --text-color from --primary-color to --secondary-color. Take a look at the logo below and toggle dark mode in your browser/OS settings. You should see the text switch from dark to light and notice the colors in dark mode being less bright.

the logo with native darkmode

Custom Darkmode (.dark/light class)

You will also notice that switching to light/dark mode manually on this blog (e.g. whith this button) will not affect the logo. As explained earlier this is because the manual override just applies to the css context and does’t actually change the prefers-color-scheme.

In the case of this blog I am adding a .light or .dark class to the <html> tag depending on the override mode.

Using the following CSS will apply the correct fill colors for our logo depending on the current mode:

:root,
:root.light {
  --primary-color: #0a0a58;
  --secondary-color: #ffffff;
  --accent-color: #ce2715;
  --text-color: var(--primary-color);
  --font-family: "Gloock";
  --font-size: 24px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --primary-color: #1b1bdd;
    --secondary-color: #bebebe;
    --accent-color: #b9200f;
    --text-color: var(--secondary-color);
  }
}

:root.dark {
  --primary-color: #00e404;
  --secondary-color: #bebebe;
  --accent-color: #b109a3;
  --text-color: var(--secondary-color);
}

This however presents us another challenge. The website’s styles do not apply to SVGs that are embedded using <img>. There are other alternatives for embedding such as <object>, <embed> or even the dated <iframe>. While using <object> or <embed> does allow some degree of accessing the SVG with JavaScript through the DOM, they are rendered with their own CSS scope. This means that any styles from your website – including any CSS variables you might have defined – don’t apply to your SVG.

Styles defined in the website’s scope are not applied to embedded SVGs (l: object, r: embed)

AS you can see, none of the styles we defined on our page are used. :root in the SVG refers to the <svg> element itself rather than the <html> and the classes .light and .dark are also not present or set in the svg.

We could solve this with some javascript trickery… or by just adding the svg straight to your website’s DOM, which already has the styles defined above attached.

...
<div class="myLogo">
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="300px">
    <g>
      <circle class="circle" cx="100" cy="80" r="80" />
      <rect class="square" width="100" height="100" x="50" y="30" />
      <path
        class="arrow"
        d="M50 142 L100 0 L167 123 C107 60 90 102  50 142  Z"
      />
      <text class="text" x="50%" y="190">ACME corp.</text>
    </g>
  </svg>
</div>
...
ACME corp.
the logo with native darkmode

Try it out: ☀️/🌙 toggle dark mode

The colors of the logo should now switch depending on the module.

Conclusion

As SVGs can be modified with css and make use of css variables we can make them reactive to darkmode or theme colors of our website, which is especially useful for logos. There are however some considerations to make on how to integrate the SVG and styles.

If you just want support for prefers-color-scheme: dark and don’t want to use CSS variables, the approach is simpler, but less flexible.

Footnotes

  1. Apart from IE of course. see