Toggle SVG icon color with dark/light theme

Take advantage of CSS variables to get fine control of SVG icon colors

 

Skip directly to the code in this codesandbox

 

CSS variables are super! Now that they are supported natively in all modern browsers there are few reasons to continue using solutions like Sass variables. Unlike with Sass, CSS variables are evaluated at run-time, not upon compilation, meaning they can be updated dynamically using JavaScript. They are also available to be consumed in any CSS rulesets. Now we can define a property in JavaScript as a CSS variable and pass it into a component. We're going to take advantage of these benefits to dynamically target and change different elements of SVG icons to make sure they look great in dark mode and light mode.

 

Before we get into it, we may need to set some things up first. Handling SVG files in React can be tricky sometimes. In this post I'm using SVG directly in .jsx files like this:

 

export default function SmileIcon() { return ( <svg viewBox="0 0 1024 1024" fill="#000000" version="1.1" xmlns="http://www.w3.org/2000/svg"> <g strokeWidth="0"></g> <g strokeLinecap="round" strokeLinejoin="round"></g> <g> <path d="M324.8 440c34.4 0 62.4-28 62.4-62.4s-28-62.4-62.4-62.4-62.4 28-62.4 62.4 28 62.4 62.4 62.4z m374.4 0c34.4 0 62.4-28 62.4-62.4s-28-62.4-62.4-62.4-62.4 28-62.4 62.4 28 62.4 62.4 62.4zM340 709.6C384 744 440.8 764.8 512 764.8s128-20.8 172-55.2c26.4-21.6 42.4-42.4 50.4-58.4 6.4-12 0.8-27.2-11.2-33.6s-27.2-0.8-33.6 11.2c-0.8 1.6-3.2 6.4-8 12-7.2 10.4-17.6 20-28.8 29.6-34.4 28-80.8 44.8-140.8 44.8s-105.6-16.8-140.8-44.8c-12-9.6-21.6-20-28.8-29.6-4-5.6-7.2-9.6-8-12-6.4-12-20.8-17.6-33.6-11.2s-17.6 20.8-11.2 33.6c8 16 24 36.8 50.4 58.4z" fill=""></path> <path d="M512 1010.4c-276.8 0-502.4-225.6-502.4-502.4S235.2 5.6 512 5.6s502.4 225.6 502.4 502.4-225.6 502.4-502.4 502.4zM512 53.6C261.6 53.6 57.6 257.6 57.6 508s204 454.4 454.4 454.4 454.4-204 454.4-454.4S762.4 53.6 512 53.6z" fill=""></path> </g> </svg> ); }

You can read about different ways to handle SVG in React in this article by LogRocket.

 

First, we need to define our colors using CSS variables. Check out the mdn web docs on CSS custom properties if you're not familiar. Here we define the default colors and then the colors for our dark theme. We swap the values for the background color and text color and brighten some colors. We also initialize a base gray hue to pass into a color function to easily control different shades (more on this below).

 

:root { --background-color: #f2f2f2; --text-color: #242424; --purple: #8277fb; --blue: #7b9dcd; --red: #d8000c; --green: #2ec126; --gray-hue: 192; --gray1: hsl(var(--gray-hue), 10%, 100%); --gray2: hsl(var(--gray-hue), 10%, 80%); --gray3: hsl(var(--gray-hue), 10%, 70%); --gray4: hsl(var(--gray-hue), 10%, 40%); } [data-theme="dark"] { --background-color: #242424; --text-color: #f2f2f2; --purple: #c2bef4; --blue: #4485e0; --red: #dc3c44; --green: #9ff69b; --gray1: hsl(var(--gray-hue), 10%, 80%); --gray2: hsl(var(--gray-hue), 10%, 60%); --gray3: hsl(var(--gray-hue), 10%, 50%); --gray4: hsl(var(--gray-hue), 10%, 20%); }

In order to activate the dark theme this way we have to set the "data-theme" attribute on the document. We can do this in React with the useState and useEffect hooks and an onClick event to change the state. This adds the "data-theme" attribute to the document and rerenders the tree when it changes. This method does not save user preference or persist the choice between sessions. To achieve that and get more in-depth, check out this article by Luke Lowrey.

 

//state to store theme choice const [useDarkMode, setUseDarkMode] = useState(false); //when theme is changed, the "data=theme" attirbute on the document is updated useEffect(() => { document.documentElement.setAttribute( "data-theme", useDarkMode ? "dark" : "" ); }, [useDarkMode]); //use this function on the onClick event in the toggle button const handleClick = () => { setUseDarkMode(!useDarkMode); };

 

Great, now our theme and colors are changing when we click the toggle button. But how can we target the different parts of our SVG icons? We could set the fill property of the SVG globally in a stylesheet, but we can get even finer control. In SmileIcon, a smiley face is rendered from two paths, the circle outline and the eyes/mouth. We want to be able to control the colors of each path and create different variations. First, lets edit the .jsx file to include some props that will let us pass in the colors.

 

export default function SmileIcon({ colors }) { return ( <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" > <g strokeWidth="0"></g> <g strokeLinecap="round" strokeLinejoin="round"></g> <g> <path d="M324.8 440c34.4 0 62.4-28 62.4-62.4s-28-62.4-62.4-62.4-62.4 28-62.4 62.4 28 62.4 62.4 62.4z m374.4 0c34.4 0 62.4-28 62.4-62.4s-28-62.4-62.4-62.4-62.4 28-62.4 62.4 28 62.4 62.4 62.4zM340 709.6C384 744 440.8 764.8 512 764.8s128-20.8 172-55.2c26.4-21.6 42.4-42.4 50.4-58.4 6.4-12 0.8-27.2-11.2-33.6s-27.2-0.8-33.6 11.2c-0.8 1.6-3.2 6.4-8 12-7.2 10.4-17.6 20-28.8 29.6-34.4 28-80.8 44.8-140.8 44.8s-105.6-16.8-140.8-44.8c-12-9.6-21.6-20-28.8-29.6-4-5.6-7.2-9.6-8-12-6.4-12-20.8-17.6-33.6-11.2s-17.6 20.8-11.2 33.6c8 16 24 36.8 50.4 58.4z" fill={colors[0]} ></path> <path d="M512 1010.4c-276.8 0-502.4-225.6-502.4-502.4S235.2 5.6 512 5.6s502.4 225.6 502.4 502.4-225.6 502.4-502.4 502.4zM512 53.6C261.6 53.6 57.6 257.6 57.6 508s204 454.4 454.4 454.4 454.4-204 454.4-454.4S762.4 53.6 512 53.6z" fill={colors[1]} ></path> </g> </svg> ); }

 

Now we can pass in an array of colors knowing that colors[0] will target the eyes/mouth and colors[1] will target the outline. In this example I have defined the colors in an object for readability and then mapped the object to an array to render them using array.map() in the JSX:

 

import SmileIcon from "../components/icons/SmileIcon"; export default function Container() { const smiles = { one: { colors: ["var(--text-color", "var(--blue"] }, two: { colors: ["var(--green", "var(--text-color"] }, three: { colors: ["var(--purple", "var(--purple"] } }; const icons = Object.keys(smiles).map((smile) => smiles[smile]); return ( <div> {icons.map((icon, i) => ( <SmileIcon key={i} colors={icon.colors} /> ))} </div> ); }

Now our icons are changing colors based on the theme. You can see how this is quite useful for more complex SVG with multiple shapes or paths. The icon will maintain shading when passed the appropriate color values. Take a look at the EmailIcon SVG. It contains several paths that let us add more detailed shading. Here we can combine CSS variables and CSS color functions to define shades of gray in a very readable and intuitive way.

export default function MailIcon({ colors }) { return ( <svg width="64px" height="64px" viewBox="0 0 24 24"> <g stroke-width="0"></g> <g stroke-linecap="round" stroke-linejoin="round"></g> <g> <g transform="translate(0 -1028.4)"> <path d="m3 1033.4c-1.1046 0-2 0.9-2 2v3 2 2 2 3c0 1.1 0.8954 2 2 2h9 9c1.105 0 2-0.9 2-2v-7-2-3c0-1.1-0.895-2-2-2h-9-9z" fill={colors[3]} ></path> <path d="m3 1032.4c-1.1046 0-2 0.9-2 2v3 2 2 2 3c0 1.1 0.8954 2 2 2h9 9c1.105 0 2-0.9 2-2v-7-2-3c0-1.1-0.895-2-2-2h-9-9z" fill={colors[1]} ></path> <path d="m3 1048.4c-0.5523 0-1.0443-0.3-1.4062-0.6l10.406-10.4 10.406 10.4c-0.362 0.3-0.854 0.6-1.406 0.6h-18z" fill={colors[2]} ></path> <path d="m1.8438 5l10.156 12 10.156-12h-20.312z" transform="translate(0 1028.4)" fill={colors[3]} ></path> <path d="m3 1032.4c-0.5523 0-1.0443 0.2-1.4062 0.6l10.406 10.4 10.406-10.4c-0.362-0.4-0.854-0.6-1.406-0.6h-18z" fill={colors[0]} ></path> </g> </g> </svg> ); }

 

We are going to use the hsl() color function to modify the lightness of a color and set different shades to pass to pass to the EmailIcon component. The hsl() function takes three arguments: the hue angle represented by a number (degrees on the color wheel), saturation represented by a percentage (in our case a very low percentage to achieve gray), and lightness represented by a percentage (50% is normal or baseline). Here we set the hue as a nice blue at 192 with the variable --gray-hue. Then we pass this variable into the hsl function for our light and dark mode variables, adjusting the saturation and lightness until we find a good balance:

/*set base hue*/ --gray-hue: 192; /*make mostly gray by reducing saturation, make shades by adjusting lightness*/ --gray1: hsl(var(--gray-hue), 10%, 100%); --gray2: hsl(var(--gray-hue), 10%, 80%); --gray3: hsl(var(--gray-hue), 10%, 70%); --gray4: hsl(var(--gray-hue), 10%, 40%); /*reduce lightness in dark theme grays by reducing lightness*/ --gray1: hsl(var(--gray-hue), 10%, 80%); --gray2: hsl(var(--gray-hue), 10%, 60%); --gray3: hsl(var(--gray-hue), 10%, 50%); --gray4: hsl(var(--gray-hue), 10%, 20%);

 

and now in Container.jsx, we import MailIcon and render it after our other icons. Here the colors are just passed directly to the component as props in an array:

//Container.jsx <MailIcon colors={[ "var(--gray1)", "var(--gray2)", "var(--gray3)", "var(--gray4)" ]} />

View this demo live in this codesandbox

 

More blog posts