Of all the wonderfully useful things the SVG image format allows us to do on the web, perhaps the most useful is exposing shapes to the web browser, so we can manipulate them powerfully with JavaScript and lighting-quick with CSS. Want to add a stroke on hover? Super easy. Or change the fill? That’s three lines of CSS.
But SVGs on the web are far from perfect. One problem—not a fatal one, but still an issue with any virtual DOM—is that embedding SVGs directly into your app can be a resource hog. No matter how much you compress, no matter how logical and streamlined your components, if you need to load up hundreds of very-complex SVGs, React will need to track all of their nodes, and updating them becomes a chore. Changing properties on thousands of these nodes in real-time becomes noticeably laggy, mostly due to React having to continually calculate and batch-update so much material. This is especially pronounced in Safari 9.x and 10.x.
One way around this is simply linking to each SVG with an <img>
tag, instead of embedding the actual SVG in the DOM. This way, the virtual DOM only needs to track one node per image, instead of hundreds for each SVG.
But in doing so we’ve crippled our ability to manipulate our SVGs. No longer can we add stroke, move shapes, remove nodes or change fill. In short, if you want :hover
to change the fill color, you’re back in the stone age. On first blush the only solution would seem to be to load up a different color SVG! This is downright primitive. What are we? Cavemen??
The answer, and the reason I wrote this article, is that no, you don’t have to go back to the neolithic, because our forebears invented CSS filter
.
Although mostly used for gaudy blurs and overworked contrast levels, we can use these filter rules to—in effect—manipulate the SVG’s fill. All it takes is a bit of patience to get the properties just right, but it’s entirely possible.
Let’s say you want to change the fill color on a transparent-background SVG from black to blue on hover.
First, let’s target the image with a CSS rule and invert the color, from black to white, using invert()
:
.svg { filter: invert(.5); }
The amount you invert will be the lightness of the final color. For this shade of blue we’re inverting 50%, giving us a medium-gray base to work with.
Now, the crux of the whole trick: If filter
provided us with some kind of coloration()
or hue()
property, this would have been much simpler. But no, as of 2017 this doesn’t exist, so we have to hack it. Instead, we can use sepia()
to turn our gray into a brown:
.svg { filter: invert(.5) sepia(1); }
So here we are, finally with some color we can manipulate. Using saturate()
and hue-rotate()
, we can fiddle with these values until we arrive at the desired color. These can be visually adjusted until you get the right shade, or inspected in software like Photoshop and precisely reverse-engineered. To get a nice bright blue, I used these values:
.svg { filter: invert(.5) sepia(1) saturate(5) hue-rotate(175deg); }
And that’s it! Check my CodePen example, and play around with it to see how you can manipulate the fill color. You now have a super-fast, CSS-only way to change the fill of an SVG, even in an <img>
tag, supported by every major browser (except IE). Enjoy!