🅭

The best method for embedding dark-mode friendly SVG in HTML

In this article, you’ll learn about the different methods for embedding Scalable Vector Graphic (SVG) images onto webpages. I’ll discuss which methods support the various security and processing modes in SVG, which methods support dark mode using @media queries, and which are the most caching friendly. I’ll also touch on why the most backward-compatible embedding method is the worst for perceived performance and has the most accessibility bugs.

SVG images have many benefits over other image formats. The file sizes can be tiny, they can be dynamic and interactive, and they can scale and adapt to fit any canvas size or application. However, web browser support for the format is incomplete, which further complicates SVG on the web.

Let’s start with a quick refresher on the two SVG processing modes relevant to the web. Secure animated processing mode (SAPM) is the most-used and also the least feature-rich. As the name suggests, SAPM is a security-tightened subset of the SVG standard. It supports both native SVG- and CSS keyframe animations. It doesn’t support script execution, responding to interactions like clicks, nor remote loading of assets like fonts or other images. If you need any of these features, you need the dynamic interactive processing mode (DIPM).

SVG images are processed in SAPM when embedded in webpages via the img element, or CSS background-image style declarations; or in SVG images via the image element. SVG images are processed as DIPM when embedded via the iframe, object, or svg elements, or in SVG images via the use element.

The use element is restricted to only load assets from the same origin. It has other restrictions too, but I’ll get back to those later in the article.

You can’t change the processing mode of an element; which mode you require dictates what method you use to embed an SVG on a webpage. (Strictly speaking, you can add restrictions on SVG images loaded in iframe elements with the sandbox attribute. However, browser support isn’t great.)

Let’s create a simple dark-mode capable SVG image with no fancy features. The following example simply creates a white canvas in default/light mode, or a black canvas in dark mode.

<svg xmlns="http://www.w3.org/2000/svg">
  <defs>
    <style>
      svg {
        background-color: white;
        color-scheme:light dark;
      }
      @media (prefers-color-scheme:dark) {
        svg {
          background-color: black;
        }
      }
    </style>
  </defs>
</svg>

As you can see, SVG uses the same CSS and @media queries as you’d use in HTML to enable dark mode. If you didn’t already know, you can use CSS to modify presentational attributes in SVG images such as fill and stroke-width.

The above image doesn’t need any external assets or interactivity, so we can load it in SAPM via an img element. The image doesn’t specify a size (it’s an infinite canvas), so we set the image dimensions in HTML instead. It’s considered best practice to do this anyway as it reduces unwanted layout shifts.

<img src="example.svg"
     height="100"
     width="200">

The SVG image will now respond to color preference scheme changes independently of the webpage that embeds it. You should make sure to test your images to make sure they work in your dark and light mode designs, and independently of the rest of the page. You can be sneaky with strokes matching the background color to ensure key graphics remain distinctly visible against different background colors. Or you can explicitly set the SVGs background color as shown above.

The above holds especially true for “dark-mode SVG favicons.” There are dozens of web browsers out there and they’ll use favicons against light, gray, and dark backgrounds regardless of the device’s color scheme preference. Make sure your icons have an outline, so they’ll work against any background!

This method of embedding SVG images will work in most web browsers going back a decade. It works flawlessly in recent versions of Firefox. However, other browsers may struggle when it comes to processing @media queries in an embedded image.

It mostly works in Chromium. However, it has a known bug (issue #1093736) where it doesn’t redraw image buffers when @media queries change. It will work most of the time, but it won’t respond if the color preference changes, e.g. on a daylight timer. You can work around this bug by forcing your SVG images to redraw when the preferred color scheme changes. (Help yourself to my example JavaScript implementation.)

Chromium doesn’t evaluate the media attribute of style elements in SVGs rendered in SAPM. You must use @media queries inside a style element instead of applying a media attribute directly on the element.

Unfortunately, Safari doesn’t support @media queries or <style media> in its image buffers (issue #199134). (It supports a few queries, but prefers-color-scheme isn’t one of them.) There are no workarounds to get this working in Safari.

However, even without support in Safari — this method is still the preferred way to embed SVG images in HTML. It works well with the browser cache and the processing and animations are well-optimized in all modern browsers. You should think of dark-mode support in images as a progressive enhancement. As mentioned before, you should ensure your transparent SVG images work against different background colors or explicitly give them a background color.

There are a few alternatives to consider if you absolutely need to support Safari. This is where the neat and easy part of the article ends. From here on out; there be dragons. You’ve been warned.

You can use the iframe and object elements to load SVG images in an HTML document. Using an object element is the best-supported method when considering ancient web browsers. These elements create descendant documents/browsing context that load your SVG in DIPM. SVG images loaded as descendant documents work well with the browser disk cache, but don’t work with the memory history cache (back/forward navigation cache). Perceived image loading performance will tank compared to loading the same image via the better optimized img element.

There’s also a minefield of accessibility issues related to SVG in descendant documents. I’ll illustrate the scope of the problems by trying to get the browser to treat an SVG in a descendant document as a non-interactive image.

A descendant browsing context can receive keyboard focus, e.g. using Tab navigation. In some browsers, you need to press the Escape key repeatedly, or another shortcut sequence to regain keyboard focus to the top-level document. This behavior is unexpected when the descendent document doesn’t have any interactive elements and you find yourself stuck inside a seemingly empty document.

Getting out can be difficult, so let us try to prevent the element from receiving focus in the first place. I’ll note here that this is actually an impossible task. To make any HTML element unfocusable, it must either be disabled or made inert. The HTML specification doesn’t provide any way to achieve this for neither iframe nor object elements. But let’s not make spec-wise impossible the enemy of attempting!

The following example code has several interesting properties: It tells accessibility tools that it’s an image and lies to them and pretends it has been disabled, even though it hasn’t. It also tells the browser to remove it from the Tab navigation order, although everything from accessibility tools to spatial navigation on TVs may ignore it. Lastly, it suppresses pointer-events — such as clicks — enabling its parent link element to receive them instead.

<a href="#">
  <object
    aria-disabled="true"
    data="example.svg"
    role="img"
    style="pointer-events:none"
    tabindex="-1"
    type="image/svg+xml">
  </object>
</a>

That’s quite a lot of attributes just to try to convince browsers and assistive tools that a descendant document is an image. You’re needlessly “reinventing the hyperlink.”

Despite all of the good intentions and virtue-signaling in the above example, I’m sure you won’t need much time to find ways to move focus into the descendant document. The only way to properly handle this is to embed a script in your SVG image that responds to receiving focus by moving it back to the parent document.

As mentioned at the start of the article, the object element processes SVG images in DIPM. It’s overkill for simple self-contained and non-interactive images. Especially considering all its potential accessibility problems. It’s difficult to get interactivity and especially keyboard navigation to work right in a descendant document. When you suppress keyboard and pointer events, you also lose out on some of the format’s benefits; like text selection and interactivity.

Chromium gets confused when you switching between light and dark mode with an object. It automatically redraws the SVG in the object, but it loses transparent backgrounds in favor of the default white (light mode) or black background colors (dark mode). You can work around this issue in the same way as discussed for the img element earlier in the article. However, this time it will have a noticeable performance impact even for trivial images on modern devices.

Using the object element for SVG is unadvisable unless you require support with ancient web browsers. It may, however, be the best option if you’re working with a large-file-size SVG image with dynamic interactivity and need it to be cacheable in the browser. Expect to keep your hands busy with extensive browser and accessibility testing, though!

In HTML5 and XHTML, you have the option of including SVG images inline in your documents. It’s supported in all leading browsers from the last decade, and SVG support will be identical to other methods of embedding SVG.

Be warned, your inline SVG documents may inherit CSS from the main document. This can be both a blessing and a curse. The following example code shows a document with a blue circle that turns green in dark mode. The circle’s fill color cascades from the document root element’s color style declaration.

<html>
  <head>
    <style>
      html {
        background-color: white;
        color-scheme:light dark;
        color: green;
      }
      circle {
        fill: currentColor;
      }
    </style>
    <style media="(prefers-color-scheme:dark)">
      html {
        background-color: black;
        color: blue;
      }
    </style>
  </head>
  <body>
    <svg
      role="img"
      height="100"
      width="150"
      xmlns="http://www.w3.org/2000/svg">
      <circle r="45" cy="50" cx="75" />
    </svg>
  </body>
</html>

Inline SVG images can’t be cached, so you need to re-download them for every page on your website. Inlining isn’t suitable for large file-size images, or common assets like logos and icons; unless you’re working with a single-page webapp.

You should always specify either role=img (static or animation) or role=application (interactive) when embedding an SVG image into a webpage. The attribute helps screen readers and assistive tools make better decisions about whether they should try to interact with the image, or bypass it entirely. You can use ARIA attributes to further describe the elements of your interactive SVG images.

Inline SVG images are processed in DIPM; meaning you can load external assets, execute scripts, and really flex SVG’s muscles. Hm. Load external assets in an SVG, you say? How about loading an SVG image inside an SVG image? This could fix the inline SVG cacheability issue. Let’s explore that option.

You’ve got four options for loading images from within an SVG. I’ll ignore scripting, the CSS background-image style declaration, and the image element (equivalent to the img element in HTML). You can use those things in HTML without involving SVG. This leaves the SVG use (re-use) element.

The SVG use element is a hugely complex topic all on its own. It intersects the DOM specification, HTML specification, and SVG specification in many different areas with vaguely specified outcomes. The result is quite different behavior between the different browsers. To make things more fun, its behavior in SVG version 2 (SVG2) diverged significantly from earlier versions.

The below example loads the #svg fragment (e.g. <svg id="svg"/>) from the external image file example.svg. You must specify a fragment; which can be the entire document, a group, or a specific shape. SVG2 assumes you mean the entire document if you omit the fragment, but no browser supports this — or much else of SVG2 — at the time of writing.

The image is loaded with the legacy xlink:href attribute and the SVG2 native href attribute. The latter is one of the few features from SVG2 that has been implemented in all modern browsers. Your browser will only use one of these attributes, so you can include both to ensure browser compatibility going back over a decade. (This tip also applies to the image element.) Lastly, it positions the loaded image in the top left corner of the parent SVG element’s canvas.

<html>
  <body>
    <svg
      role="img"
      height="100"
      width="200"
      xmlns:xlink="http://www.w3.org/1999/xlink">
      <use
        href="example.svg#svg"
        xlink:href="example.svg#svg"
        x="0"
        y="0"/>
    </svg>
  </body>
</html>

The overall principle works fine in all modern browsers. However, it won’t behave as you expect (unless you’re a SVG specification author). In SVG2, this should have cloned the entire external image including its style and everything. In SVG < 2, you only clone graphical shapes.

In Firefox, you can load a complete SVG image in DIPM. It follows the SVG2 specification since version 70. SVG2 also specifies that stylesheet rules external to the use element shouldn’t cascade into it, However, Firefox ignores this part of the SVG2 (and shadow DOM) specifications and lets stylesheets in the HTML document style the SVG descendants inside the re-used external SVG image.

Chromium and Safari process the same image in SAPM. The browsers strip away any invisible element — such as animate, base, defs, linearGradient, style, and script — from the re-used SVG image. It retains inline CSS style and presentation attributes set directly on the shapes, though. This behavior conforms with the SVG < 2 specification.

Furthermore, Chromium and Safari resolve any relative links in the external SVG relative to the current document’s base location instead of relative to the image’s location. This conforms to the shadow DOM specification, but not the SVG specification. You also can’t change the base location using either the xml:base attribute or the base element. This link resolution is the exact opposite behavior of Firefox, which again follows SVG2.

SVG2 seems to be way more useful in this particular area than older versions of the specification. It’s certainly more predictable, as the image behaves the same wether loaded separately or embedded into different webpages.

Firefox treats the SVG use element like a slightly more powerful version of the HTML object element. Sans some of the security restrictions, and with different accessibility needs. The use element doesn’t have any glaring accessibility issues like the object element. However, make sure to specify its <svg role/> attribute.

In summary, the results with the use element will vary considerably unless you only it for simple shapes. Thorough browser compatibility testing will be required in any case. If you want to re-use your image assets in SVG images, then your best option is to embed it into your HTML document and load the shared assets via the simpler image element instead.

I’ve put together a quick test case you can use to test the different modes discussed in this article in different web browsers. It doesn’t include any workarounds to disguise the browser bugs discussed in this article. (It sure will highlight them, though.) You may get different results depending on whether you reload the page after switching between light/default and dark mode.

Make sure to test your website and SVG images in the Samsung Internet browser on Android. It doesn’t support @media (prefers-color-scheme:dark) like every other modern web browser. Instead, it applies its own weird color-darkening scheme. For whatever reason, its results with SVG images are often shoddy. The results may be ugly, but make sure your designs remain legible and maintain good contrast.

Lastly, I’d just like to mention that not all stand-alone SVG libraries understand @media queries. Processing may differ based on the hosting device’s color scheme preference. Others will apply style declarations from @media queries they don’t support, e.g. rsvg.

Related reading