Skip to content
Smart SVG™ by Equivalent
The site's color tokens flowing into an inline SVG At the left, a card for the website's CSS with a light and dark toggle and three named tokens: brand, accent and ink. Lines carry each token to the right, into a drawing whose shapes are filled from those same tokens, so switching the site's theme recolors the drawing. This graphic is built the same way and follows this site's theme. YOUR SITE'S CSS --brand --accent --ink INLINE SVG fill: var(--brand)

Let your site color the SVG: tokens, variables and inline graphics

· 10 min read

An illustration that ignores your dark mode has its colors baked in. Move them into your CSS, put the graphic inline, and it follows your theme like everything else.

Your site has a dark mode. The buttons follow it, the cards follow it, the headings follow it. Then there is the illustration in the middle of the page, still drawn in the light palette, sitting on a dark background like a sticker somebody forgot to peel off.

Nothing is broken, exactly. The graphic is doing what it was told. Its colors were written into the file when it was exported, and nothing your site does can reach them. This post is about moving those colors out of the file and into the place the rest of your colors already live: your CSS.

Tokens and variables, briefly

A design token is a named design decision. "Brand blue" is a token. "The color of body text" is a token. The point of the name is that the decision can change without every place that uses it changing too.

A CSS custom property, usually called a CSS variable, is how a browser carries a token. You define it once, usually on the root of the page, and read it anywhere with var():

:root { --brand: #0066cc; --accent: #00ccff; --ink: #171717; } .button { background: var(--brand); } .heading { color: var(--ink); }

Dark mode then becomes a second set of values for the same names. The button does not change. The token does, and everything that reads it follows. That is the whole trick, and an SVG can take part in it like any other element on the page, as long as two things are true: the colors are not fixed inside the file, and the file is part of the page.

Where an SVG keeps its colors

A typical export sets its colors in one or more of three places. Each one is a fixed value, and each one behaves differently when you try to override it:

<svg viewBox="0 0 64 64"> <style>.a { fill: #0066cc; }</style> <path class="a" d="..." /> <path style="fill: #00ccff" d="..." /> <path fill="#171717" d="..." /> </svg>

  • A style block inside the file. Its rules apply wherever the file goes. Inline in a page, they also join the page's stylesheet, so a class name like .a can collide with yours.
  • A style attribute on an element. Inline styles beat every ordinary rule in your stylesheet, so your tokens lose to it unless you reach for !important.
  • A fill or stroke attribute. These are presentation attributes, and they have the lowest priority of all. Any CSS rule that targets the element wins, which makes them the easiest to override and the easiest to forget are there.

Design tools tend to use all three in one file, sometimes on the same shape. Before a graphic can take your colors, every one of them has to go.

Taking the colors out

The goal is a file that says what each shape is and nothing about what color it should be. Give each shape a class that names its role, and remove the fixed values:

<svg viewBox="0 0 64 64"> <path class="brand" d="..." /> <path class="accent" d="..." /> <path class="ink" d="..." /> </svg>

Then the page decides, using the tokens it already has:

.brand { fill: var(--brand, #0066cc); } .accent { fill: var(--accent, #00ccff); } .ink { fill: var(--ink, #171717); }

The second value in each var() is a fallback. It is used only when the token is not defined, and it matters more than it looks. A fill that reads an undefined variable with no fallback does not quietly keep its old color. It behaves as if no fill were set, so the shape inherits whatever is above it, and with nothing set above it that is black. One misspelled token name and a shape turns black with no error anywhere.

Use the names your site already uses

The variable names in the graphic have to match the names in your stylesheet exactly, so name them after your tokens rather than after the color they happen to be today. --brand survives a rebrand. --blue does not.

Watch for names that carry a mode in them, like --brand-light and --brand-dark. They look tidy, but they move the light and dark decision back into the graphic: the file now has to know which mode it is in to pick a name, and your site's toggle cannot help it. One name per role, with the page supplying a different value per mode, keeps that decision where it belongs.

Smaller files are a side effect

Every color you remove is markup the file no longer carries, and a stylesheet that colors twenty graphics is written once instead of twenty times. The saving per file is usually modest. It adds up across an icon set, and the larger win is that the palette now has one home.

Following the toggle and the operating system

Most sites that offer a theme switch want two behaviors at once. With no choice made, follow the operating system. Once the reader uses the switch, follow the switch, even if it disagrees with the operating system. That fits in three blocks of CSS, and the token names never change between them:

/* 1. Light, the default */ :root { --brand: #0066cc; --ink: #171717; } /* 2. Dark operating system, unless the switch says light */ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --brand: #00ccff; --ink: #f7f7f7; } } /* 3. The switch says dark */ :root[data-theme="dark"] { --brand: #00ccff; --ink: #f7f7f7; }

The switch sets data-theme on the root element, and removes it to go back to following the operating system. The attribute name is yours to choose; data-theme, data-color-mode and a class on the html element are all common. What matters is that the graphic never sees any of this. It reads var(--brand), and the cascade has already worked out which value that is.

Two details are worth getting right. Keep a value for every token in every block, or a token defined only for dark mode will be undefined in light mode and fall back to black. And set the attribute before the page first paints, with a small script in the head, so a reader with a dark preference does not see a flash of the light palette first.

Why it has to be inline, not an <img>

This is the step that catches people out. The file is clean, the tokens are defined, and the graphic still renders in its fallback colors. Usually the reason is one line:

<img src="/illustrations/hero.svg" alt="..." />

An SVG loaded through an <img> is a separate document. The browser renders it in isolation, as a picture, and the page's styles stop at its edge. Your custom properties do not cross into it, your classes do not match inside it, and the attribute your switch sets on the page is not visible to it. The same is true of an SVG used as a CSS background-image. Only the file's own internal styles apply.

Inline SVG is different. When the markup sits in the page itself, every shape is an ordinary element of the page, and your stylesheet reaches it exactly as it reaches a button:

<div class="hero-art"> <svg viewBox="0 0 64 64" role="img" aria-labelledby="hero-title"> <title id="hero-title">A mountain at sunrise</title> <path class="brand" d="..." /> <path class="accent" d="..." /> </svg> </div>

You rarely have to paste markup by hand. Most frameworks can import an SVG file as a component at build time, and a server-side template can include the file's contents directly. Either way, what reaches the browser is inline markup, which is the part that matters.

When an <img> is the right call

An <img> is not wrong. It caches separately, it keeps large or complex artwork out of your HTML, and it is the only option in places that do not run your CSS at all, like email. A file used that way can still adapt, with its own prefers-color-scheme rules inside it, but it can only respond to what the browser reports to it. It will never follow your switch. Choose inline when the graphic should look like part of the interface, and an image when it should look the same everywhere.

Making sure it renders correctly

Inlining moves the SVG into your page, so a few things that never mattered for an image start to matter:

  • Duplicate IDs. Gradients, clip paths and title elements are referenced by id. Inline the same graphic twice, or two graphics exported by the same tool, and the ids collide. The second reference silently points at the first definition. Give each inline copy unique ids, which many build tools can do for you.
  • Untrusted files. An inline SVG can carry script and event handlers that an <img> never runs. Only inline files you produced or have sanitized.
  • An accessible name. Give a meaningful graphic role="img" and a title referenced by aria-labelledby, so it is announced with a name. Give a decorative one aria-hidden="true" so it is skipped.
  • Forced colors. In Windows high contrast and similar modes the browser replaces your palette with the reader's own. Check that shapes still read, and use system colors such as CanvasText in a forced-colors block where they do not.

Then test the combinations, not just the defaults:

  • Light operating system, switch untouched: light palette.
  • Dark operating system, switch untouched: dark palette.
  • Dark operating system, switch set to light: light palette. This is the one that most often breaks.
  • Light operating system, switch set to dark: dark palette.
  • Inspect a shape in your browser's developer tools. Its fill should come from your stylesheet, not from an attribute or a style on the element.
  • Open the file on its own. It should still render, in its fallback colors.

Where Smart SVG™ files fit

A Smart SVG™ keeps its palette as custom properties near the top of the file rather than as fixed values scattered through it, which is the starting point everything in this post builds on. On its own, as an image, the file uses its built-in values and its own media queries.

Whatever produces your graphics, the rule is the same. Colors belong to the page, the file names its shapes, and the graphic goes inline wherever it should follow your theme.


Post Details

Published

· 10 min read

The file names its shapes. The page decides.

A typical export hides fixed colors in a style block, in style attributes and in fill attributes. Each one ignores your theme, and the style attribute beats your stylesheet outright.

Take them out, give each shape a class that names its role, and color those classes from your tokens in your own CSS. Keep a fallback in every var() so a missing token does not turn a shape black.

The same SVG with colors baked in, and with colors from the page's tokens Two code panels. Before: the file sets its colors in three places, an embedded style block, a style attribute and a fill attribute, each holding a fixed hex value that ignores the site's theme. After: the file only names each shape with a class, and three rules in the page's own stylesheet fill those classes from the brand, accent and ink tokens, each with a fallback value. BEFORE: COLORS BAKED IN <svg viewBox="0 0 64 64"> <style>.a{fill:#0066cc}</style> <path class="a" d="…"/> <path style="fill:#00ccff" d="…"/> <path fill="#171717" d="…"/> </svg> 1 2 3 1 style block, 2 style attribute, 3 fill attribute Three fixed values. None of them hears the toggle. AFTER: COLORS FROM YOUR TOKENS <svg viewBox="0 0 64 64"> <path class="brand" d="…"/> <path class="accent" d="…"/> <path class="ink" d="…"/> </svg> No hex values and no style attributes. Each shape only says what it is. AND IN YOUR SITE'S STYLESHEET .brand { fill: var(--brand, #0066cc); } .accent { fill: var(--accent, #00ccff); } .ink { fill: var(--ink, #171717); } The page owns the values. The second value is a fallback.

The SVG never asks which setting won. It reads the token.

The operating system sets a preference, and your theme switch overrides it once it has been used. Three blocks of CSS turn that into token values: light by default, dark for a dark system unless the switch says light, and dark whenever the switch says dark.

The graphic sees none of it. It reads var(--brand), and the cascade has already decided what that is.

How the operating system setting and the site toggle reach an inline SVG Three stages from left to right. First, what the reader chose: an operating system color preference, and the site's own theme toggle, which wins when it has been used. Second, the site's stylesheet, which sets the token values for light mode, overrides them for a dark operating system unless the toggle says light, and overrides them again when the toggle says dark. Third, the inline drawing, which only reads the token values and never needs to know which setting produced them. 1 · THE READER Operating system Light or dark, set once for every app. Site toggle WINS Set on this site. Once used, it beats the OS until reset. 2 · YOUR CSS :root { --brand: #0066cc; } Light, the default. @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --brand: #00ccff; } } Dark OS, unless the toggle said light. [data-theme="dark"] { --brand: #00ccff; } The toggle said dark. Same token names in every block. 3 · THE SVG fill: var(--brand) The SVG never asks which setting won. It reads the token. Change the rules in one stylesheet and every inline graphic follows, along with every button, card and heading that reads the same tokens.

To follow your theme, it has to be inline.

An SVG in an <img>, or used as a CSS background, is a separate document. Your variables, classes and theme switch stop at its edge, and it keeps the colors it was exported with.

Inline, every shape is part of the page, so your stylesheet reaches it like any other element. Give each copy unique ids, only inline files you trust, and give it an accessible name or hide it if it is decorative.

An SVG in an img element compared with the same SVG inline Two web pages side by side, each sending its color tokens toward the same graphic. On the left the graphic is loaded with an img element. It is a separate document behind a solid wall, the tokens stop at the wall, and the graphic keeps its own colors. On the right the graphic's markup is inline in the page. There is no wall, the tokens flow straight in, and the graphic takes the page's colors and follows its theme toggle. LOADED AS AN IMAGE <img src="logo.svg"> page tokens its own document INLINE IN THE PAGE <svg>…</svg> page tokens part of the page Keeps its own colors Only its own <style> applies. Your variables, classes and toggle stop at the edge. The same is true of CSS background images. Takes your colors Your stylesheet reaches every shape, so the graphic follows the toggle and the OS with the rest of the page.

Questions about coloring SVGs from CSS

Almost always one of two reasons. Either the file still sets its own colors in a style attribute, which beats your stylesheet, or it is loaded through an img element or a CSS background, which keeps the page's styles out entirely. Check both before changing anything else.

It is reading a variable that is not defined, and the var() has no fallback. The shape then behaves as if it had no fill and inherits black. Check the token name for typos, check that the token is defined in every theme block, and add a fallback value.

Yes, with a prefers-color-scheme block inside the file. It responds to the preference the browser reports to it. What it cannot do is follow your site's own theme switch or read your tokens, because the page's styles never reach it.

It is part of the HTML, so it is not cached separately and it is downloaded again with each page. For icons and small illustrations that is usually a fair trade, and removing the baked-in colors makes each file smaller. For very large or detailed artwork that does not need to follow your theme, an img is often the better choice.