One of the main benefits of using Scalable Vector Graphics (SVG) is what its name suggests: Scalability. When working on a WebApp UI, we usually want:
- Responsive sizes
- Accessibility
Using SVG for graphics can greatly improve responsiveness. With them, size really doesn’t matter.
You can scale them up or down without hurting quality, and they hardly affect file size. Well… the file size issue isn’t entirely accurate for very detailed, complex images. But we will get to it in a bit.
Part of making the UI accessible is color contrast. You don’t need to pixel-dive when modifying SVG. The colors are set as text instructions. Much like the way it works with CSS.
So why not use SVGs?
I started by praising SVGs, and I’ll probably continue praising them. However, where there are pros, there are also cons. A good decision is based on both.
- They can be harder to create
- Not suitable for photos or very complex illustrations.
- Can introduce some security risks
The learning curve
SVG has lots of features, and mastering them all takes a while. Even without becoming an SVG guru. Even when you don’t need to use any special capability, drawing and modifying it can be a pain.
Yes, many tools can create both raster images (png, gif, jpeg, webp, and so on) and vector-based graphics like SVG. Unless you are very good with the pen tools in those apps, you’ll run into issues with vector graphics. Mastering the pen tools takes time and practice.
You might say, “But my app has trace ability and will convert my image to a vector”. That will usually result in a bloated SVG file. And the result usually won’t have the same quality.
That said, a steep learning curve shouldn’t deter you. But it isn’t suitable if you don’t have time to learn it.
Photo and complex graphics
For that matter, I’ll refer to photos, realistic images, or images with lots of details and curves as the same thing. SVG is a set of text instructions like:
- Draw a line from this point to that point.
- Fill the area a certain way.
A raster image tells us this pixel is this color. A vector image needs to tell us: Go from here to there and paint it like this. Imagine how many more instructions it will need if the image has many curves and color changes. So while image size doesn’t matter to vectors, the number of details does. Also, SVGs have their own coordinate system, so every instruction can require math calculations. It is like giving end users extra math homework for their CPU.
Security issues
This is the scary part, but don’t worry, everything I’m about to tell you can be mitigated. That is, if you are aware and take a few little steps.
One good feature of SVG is that it can contain scripts. A script is like a program that runs and gives the computer instructions. If the instructions are malicious, they can do things that we don’t want. This can also happen with a compromised script or certain user input. Meaning, it’s something to mitigate anyway.
So what do we do?
Simple: Don’t let users upload SVG to your site. I’m only joking. Just like I won’t tell a user to never connect to the internet, never insert any discs or USB to their computer, and keep it in a very secure location with its own electricity network. I won’t tell you not to let people upload such files. What I will say is that if and when you enable it, make sure to:
- Sanitize the files. Use a program/script to remove anything that isn’t graphic instructions. No downloads, no external objects, no iframes, and such.
- Turn into a raster image. If you allow different image formats, consider using some server-side script to convert the SVG into a raster image.
-
Always display it as an image. Whether as
<img>or abackground-image, it tells the browser to treat it as an image, not a script. -
Enforce serving SVG files with certain HTTP headers:
Content-Type: image/svg+xmlContent-Security-Policy: script-src 'none'
- Configure any XML/SVG parsing tool on your server to disable XML DTD processing.
Make sure to never skip the first step if you allow SVG uploads. Like any user provided input, sanitize it. Following the other steps without the sanitization is not recommended.
If you wish to learn more about dealing with risks with files uploads in general (not just SVG), check out the File Upload Cheat Sheet by OWASP.
In short, it’s like any technology running on the internet today.
Anything stopping us from recoloring?
After all, this is what this guide is about.
As you probably noticed, when you load a single-color icon/symbol/glyph in SVG format. You usually see it as a button or near text. They can inherit the container’s color because they use currentColor for their color instructions. But that is assuming the SVG is part of our page and isn’t loaded via a URL. When it is loaded from a URL, it runs in its own little world and doesn’t know anything about the colors we use. It is an outsider, and the browser guards protect us by not giving it any details. And when the SVG has multiple colors, the currentColor magic can’t cover them all.
Becoming part of our site
An outsider doesn’t need to be an outsider if we accept the outsider into our society. In this case, our HTML or CSS. It can understand our ways.
The question is how to make it part of the site. As long as it’s a URL, no integration will resolve it. The SVG will keep running in its own space. However, if we add the SVG to our CSS as image data, or embed it in our HTML as an element, things can get more interesting. As HTML, it will be an integral part of our site, with a lot more control over it. As a data URL in the CSS/HTML it is still not fully part of the site, it still has its own space, but we can manipulate it, we have control over the data URL string. And if the SVG is prepared properly, we can even control the colors it exposes. It is like creating a contract, or a custom API, between the site and the SVG.
Using colors
Before we dive into sharing the colors we use with the SVG, we need to control our colors properly.
You might have run into CSS variables, especially if you ever worked with a CSS framework, design systems, or certain scripts. CSS allows us to define variables like:
:root {
--primary-color: #2563eb;
--secondary-color: #14b8a6;
--accent-color: #0080ff;
}
/* Also to an element */
.certain-elements {
--primary-color: #ff8000;
}
h1 {
color: var(--accent-color);
}
You can use those variables in many different ways. You can define any CSS setting this way. They give you a special power. Used properly, they let you control your entire site’s appearance. They are often used to define the site colors.
Want to change --primary-color at runtime? You can use JavaScript to replace the value. For example:
document.documentElement.style.setProperty('--primary-color', '#6B46C1');
CSS variables are a powerful tool worth adding to your belt. If you want to learn more about them, check out CSS Variables: (A Complete Guide) on Scrimba.
Preparing the SVG
As mentioned, we need to create “a contract” between the site and the SVG that joins it. We do that by agreeing on a set of CSS variables that the SVG uses and the site configures.
Say I have a simple SVG file that uses only 2 colors:
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>
<path d='M0-10 2.35-3.24 9.51-3.09 3.8 1.24 5.88 8.09 0 4-5.88 8.09-3.8 1.24-9.51-3.09-2.35-3.24Z'
transform='translate(50 50) scale(2.3)' fill='#9e3548' stroke='#6f2031' stroke-width='1.5'
stroke-linejoin='round'/>
</svg>
As you can see, the SVG uses one fill color (#9e3548) and one stroke color (#6f2031). This code is not prepared. It can be encoded for use in CSS, but it hard-codes its own color. To prepare it, we need to add some variables it can use.
SVG files support the <style> tag, which in turn means they support CSS. That doesn’t mean they know the site CSS. As we said, unless it is fully integrated into the site code, it doesn’t know. Meaning, if it is loaded via a URL, it doesn’t have access to those variables. This also applies to the site. It can’t control the SVG variables if the code is not part of it.
So if we are to add some variables to prepare our SVG, it will look something like this:
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>
<path d='M0-10 2.35-3.24 9.51-3.09 3.8 1.24 5.88 8.09 0 4-5.88 8.09-3.8 1.24-9.51-3.09-2.35-3.24Z'
transform='translate(50 50) scale(2.3)' fill='var(--svg-fill, #9e3548)' stroke='var(--svg-stroke, #6f2031)' stroke-width='1.5'
stroke-linejoin='round'/>
</svg>
The difference? Instead of stroke='#6f2031' and fill='#9e3548', now it is: stroke='var(--svg-stroke, #6f2031)' and fill='var(--svg-fill, #9e3548)'. The variable names don’t matter, as long as the site knows about them.
At this point our SVG looks like:
Preparing the site
First, before the site can affect the SVG code, the SVG code needs to be part of the site. We can add it to the HTML itself like:
<div>
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>
<path d='M0-10 2.35-3.24 9.51-3.09 3.8 1.24 5.88 8.09 0 4-5.88 8.09-3.8 1.24-9.51-3.09-2.35-3.24Z'
transform='translate(50 50) scale(2.3)' fill='var(--svg-fill, #9e3548)' stroke='var(--svg-stroke, #6f2031)' stroke-width='1.5'
stroke-linejoin='round'/>
</svg>
</div>
Or we can add it to our CSS as a background-image. We just need to encode it first. So how do we encode it? Some websites can do that. Or, we can run a simple snippet in our browser’s developer console:
const svg = `placeholder`;
const dataUrl = `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
console.log(dataUrl);
Just need to remember to replace the SVG with the actual SVG code.
Once we have the result, we can add it to the CSS file.
button.star {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath d='M0-10 2.35-3.24 9.51-3.09 3.8 1.24 5.88 8.09 0 4-5.88 8.09-3.8 1.24-9.51-3.09-2.35-3.24Z' transform='translate(50 50) scale(2.3)' fill='var(--svg-fill%2C %239e3548)' stroke='var(--svg-stroke%2C %236f2031)' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E");
width: 100px;
height: 100px;
background-position: 0 0;
background-repeat: no-repeat;
background-size: 100% 100%;
}
Then, if we have a <button> with the class star, it will display our SVG (which happens to be a star). A little accessibility note: Using SVG as our button background doesn’t tell screen-readers anything about the button. We still need to make sure to add an accessible label to the button.
We can also enter the encoded SVG inside an image src attribute. But for this guide, I’ll use the background method.
Looking at the CSS and imagining I have other places with different SVGs, I get the feeling it is going to get ugly and not fun to maintain. That is another thing the CSS vars are good for. I can have:
:root {
--svg-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath d='M0-10 2.35-3.24 9.51-3.09 3.8 1.24 5.88 8.09 0 4-5.88 8.09-3.8 1.24-9.51-3.09-2.35-3.24Z' transform='translate(50 50) scale(2.3)' fill='var(--svg-fill%2C %239e3548)' stroke='var(--svg-stroke%2C %236f2031)' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E");
}
button.star {
background-image: var(--svg-image);
width: 100px;
height: 100px;
background-position: 0 0;
background-repeat: no-repeat;
background-size: 100% 100%;
}
Moving it to a central place and using the variable name alone makes it cleaner, even with many more icons. Especially since you can load the tokens from a different stylesheet embedded in the main one. For example, you can have a tokens.css file with the variables and a main.css with the rest. Just make sure to load both or import the tokens in the main one: @import "tokens.css".
It would be nice if we could say, “OK, now we probably just need to add the CSS vars, and we are done.” Well… Almost. But you are correct. We also need to add the color variables. The SVG sets its own default colors. I’m going to set different color values in the CSS file. Our goal is to control the colors after the SVG has been created, without updating it. So let’s quickly add that.
:root {
--primary-color: #000066;
--svg-fill: #0000cc;
--svg-stroke: var(--primary-color);
}
The values here don’t matter much, since it is just to show the change.
At this point, we’re almost ready. The SVG is prepared. The CSS sets the variables. We just need one more thing.
Making it work
When a browser loads the SVG data as a background image, it loads the SVG’s color values and rasterizes it as an image. Since it is an image, the colors are set. Now we need to update the SVG data, since the colors might have changed. But remember, our goal is to set the colors in one place. We don’t want to go back to the original SVG and update the file. We want to update the colors only in one place.
JavaScript to the rescue!
const root = document.documentElement;
const dataUrlPrefix = "data:image/svg+xml,";
const svgDataURL = getComputedStyle(root)
.getPropertyValue("--svg-image")
.trim();
const urlMatch = svgDataURL.match(
/^url\(\s*(["']?)(data:image\/svg\+xml,.*?)\1\s*\)$/s,
);
if (!urlMatch) {
throw new Error("--svg-image does not contain an SVG data URL.");
}
const svgTemplate = decodeURIComponent(
urlMatch[2].slice(dataUrlPrefix.length),
);
function updateSvgImage() {
const fill = getComputedStyle(root)
.getPropertyValue("--svg-fill")
.trim();
const stroke = getComputedStyle(root)
.getPropertyValue("--svg-stroke")
.trim();
const resolvedSvg = svgTemplate
.replace(
/var\(\s*--svg-fill\s*,\s*[^)]+\)/g,
() => fill,
).replace(
/var\(\s*--svg-stroke\s*,\s*[^)]+\)/g,
() => stroke,
);
root.style.setProperty(
"--svg-image", `url("data:image/svg+xml,${encodeURIComponent(resolvedSvg)}")`,
);
}
updateSvgImage();
So what did we do here?
- We get the SVG data URL from the CSS vars.
- We save the original decoded data in our global scope so we can always access it.
- We run a function,
updateSvgImage, that reads the CSS variables from our root element. - The function then creates a new data string from the decoded template and replaces the color values.
- It then updates the background image variable with the new SVG data string.
Time to see it at work. Click the button below to see the star change colors. And if you scroll up, you will see it also changed the star above.
Try the recoloring code. Each click changes the two CSS color variables, then rebuilds the background image from the untouched SVG template.
Time to get serious
The code above showed a concept, and we saw it working. However, it’s still far from ready. What does it need? Well, as a start:
- Several multi-color SVGs
- Ability to work with several multi-color SVGs
- Safe guards
- and triggers
So let’s do a quick improvement run, focusing on the important parts. I’ll use 3 multi-color SVG illustrations, 2 CSS files, and 2 JavaScript files. The full source is available to download, so I’ll focus only on the recoloring script.
The recoloring
The demo includes the code required for it to work, mostly the UI. And the recoloring logic is now centered in a single file: recoloring.js. This file can work as-is on other pages and sites and handle SVG background recoloring (assuming the page/site is prepared for it). I don’t recommend using it as-is. I built it with this guide in mind, and it’s far from production-ready.
So without further ado, let’s see what recoloring.js does.
Utilities for Recoloring
We will need to decode and encode the SVG(s) data URL(s). Although the values are provided in the CSS, we saw above that we save a template for each one. Before decoding, we should make sure we are dealing with a valid encoding type. That is, it should be an SVG image, or at least present itself as one. And it should be encoded as regular URL encoding and not something like base64.
const svgDataUrl = {
decode(cssValue) {
const match = cssValue.trim().match(/^url\(\s*(["']?)(data:image\/svg\+xml,.*?)\1\s*\)$/s,);
if (!match) {
throw new Error(`Expected an SVG data URL, received: ${cssValue}`);
}
return {
cssValue: match[0],
svg: decodeURIComponent(
match[2].slice(dataUrlPrefix.length),
),
};
},
encode(svg) {
return `url("${dataUrlPrefix}${encodeURIComponent(svg)}")`;
},
};
A little autonomy
We want our script to detect SVGs on its own, so we won’t need to update everytime we re-design our page. We can do this easily with the CSSOM (CSS Object Model). Think of the CSSOM as the HTML DOM. It gives JavaScript the API to handle the CSS.
function collectSvgPropertiesFromStyle(style, propertyNames) {
for (let index = 0; index < style.length; index += 1) {
const propertyName = style.item(index);
if (propertyName.startsWith(svgPropertyPrefix)) {
propertyNames.add(propertyName);
}
}
}
function collectSvgPropertiesFromRules(rules, propertyNames) {
for (const rule of rules) {
if (rule instanceof CSSStyleRule && rule.selectorText === ":root") {
collectSvgPropertiesFromStyle(rule.style, propertyNames);
}
if (rule instanceof CSSImportRule && rule.styleSheet) {
collectSvgPropertiesFromSheet(rule.styleSheet, propertyNames);
} else if ("cssRules" in rule) {
collectSvgPropertiesFromRules(rule.cssRules, propertyNames);
}
}
}
function collectSvgPropertiesFromSheet(styleSheet, propertyNames) {
try {
collectSvgPropertiesFromRules(styleSheet.cssRules, propertyNames);
} catch {
}
}
function findSvgProperties() {
const propertyNames = new Set();
for (const styleSheet of document.styleSheets) {
collectSvgPropertiesFromSheet(styleSheet, propertyNames);
}
collectSvgPropertiesFromStyle(root.style, propertyNames);
return [...propertyNames];
}
Our code here uses a bit of recursion, since there are a few different places that can contain the :root properties, like different stylesheets, different CSS layers, and such.
Watching for Changes
We also need to monitor for changes so we can re-trigger recoloring when needed. For example, changes can happen to HTML element classes or style attributes.
function scheduleRefresh() {
if (refreshScheduled) {
return;
}
refreshScheduled = true;
requestAnimationFrame(() => {
refreshScheduled = false;
recolorSvgImages();
});
}
function observeColorChanges() {
new MutationObserver(scheduleRefresh).observe(root, {
attributeFilter: ["class", "style"],
attributes: true
});
window.addEventListener("resize", scheduleRefresh);
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", scheduleRefresh);
}
Using requestAnimationFrame, we tell the browser to apply the changes before it repaints the page. It is optimized for redrawing and usually doesn’t run if the browser tab is not in focus.
The resize listener is useful because viewport changes may activate media queries, which in turn, might change the CSS color tokens. We also listen for color scheme changes so we can cover light/dark mode changes.
Using the MutationObserver API, we can listen for DOM changes, so if the root element’s class or style attributes change, we ensure the color changes still apply. This is also a common way sites handle dark and light modes and some layout changes.
In short, we listen for the needed changes so we can ask the browser to update our SVG colors when they happen.
Doing the Work
Now that we know what the script needs, we can review the main logic.
function synchronizeProperties() {
const svgProperties = findSvgProperties();
const computedStyles = getComputedStyle(root);
let foundNewTemplate = false;
imageProperties = svgProperties.filter((propertyName) =>
propertyName.startsWith(imagePropertyPrefix)
)
;
colorProperties = svgProperties.filter((propertyName) =>
propertyName.startsWith(colorPropertyPrefix),
)
;
for (const propertyName of imageProperties) {
if (svgTemplates.has(propertyName)) {
continue;
}
try {
const cssValue = computedStyles.getPropertyValue(propertyName);
svgTemplates.set(
propertyName,
svgDataUrl.decode(cssValue)
);
foundNewTemplate = true;
} catch (error) {
console.warn(`Could not read ${propertyName}.`, error);
}
}
return foundNewTemplate;
}
function colorSignature(computedStyles) {
return colorProperties
.map((propertyName) => {
const value = computedStyles
.getPropertyValue(propertyName)
.trim();
return `${propertyName}:${value}`;
}
)
.
join(";");
}
function createResult(updated) {
return {
colorCount: colorProperties.length,
imageCount: svgTemplates.size,
updated
};
}
/**
* CSS tokens on the page cannot cross into an SVG background image's
* document. Replace the var() references and then encode the SVG again.
*/
function recolorSvgImages(force = false) {
const foundNewTemplate = synchronizeProperties();
const computedStyles = getComputedStyle(root);
const signature = colorSignature(computedStyles);
if (!force && !foundNewTemplate && signature === lastColorSignature) {
return createResult(false);
}
for (const [propertyName, template] of svgTemplates) {
const recoloredSvg = template.svg.replace(
/var\(\s*(--svg-color-[\w-]+)\s*,\s*([^)]+?)\s*\)/g,
(match, colorProperty, fallback) => {
const color = computedStyles
.getPropertyValue(colorProperty)
.trim();
return color || fallback.trim();
}
);
root.style.setProperty(
propertyName,
svgDataUrl.encode(recoloredSvg)
);
}
lastColorSignature = signature;
const result = createResult(true);
window.dispatchEvent(
new CustomEvent("svg-recoloring-updated", {detail: result})
);
return result;
}
Ok, you got me. It’s the main logic plus some gravy on top. It also includes a nice debugging method that reports a brief summary of the changes after they happen.
The recolorSvgImages function is the actual engine behind it all. It first verifies that we really need to run the recoloring. And if we do, it updates the SVG(s) data URL(s) with the color properties’ values.
Starting the engine
So far, we’ve covered what is needed and the actual logic. However, we still need something to start it all.
function initialize() {
if (initialized) {
return;
}
initialized = true;
const result = recolorSvgImages(true);
observeColorChanges();
resolveReady(result);
window.dispatchEvent(
new CustomEvent("svg-recoloring-ready", { detail: result }),
);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initialize, {
once: true,
});
} else {
initialize();
}
This part is pretty straightforward. If the page is still loading, make sure to run it once it is ready. If it was already loaded, go ahead and run initialize so we do the recoloring and add our listeners.
Being Nice to our Neighbors
We are also being friendly and letting anyone who wants to know that we’ve done our thing and are ready. To be even friendlier, we let any external script trigger the recoloring. This covers us if other scripts on the page change things that affect the colors, but we don’t detect it.
window.svgRecoloring = {
ready,
refresh: recolorSvgImages,
getColorProperties() {
return [...colorProperties];
},
getOriginalImage(propertyName) {
return svgTemplates.get(propertyName)?.cssValue ?? "";
}
};
A little look to the future
While building recoloring.js was fun, it might not be relevant for much longer. CSS Linked Parameters now proposes native external-SVG parameters, but it is still experimental and unavailable in major stable browsers. You can read more about it in the W3C draft: CSS Linked Parameters draft. And in the link-parameters CSS property in MDN.
Final Words
That’s about it. We now have a semi-robust way to handle the color changes in our SVGs. If you want to play around with it, check out the live demo. It lets you see the changes on page load and tweak colors at runtime, too. So have fun re-coloring the SVGs in the demo in new, creative ways.