Ways to color svg icons in React

/ 1 Views / Blog

SVGR

I use SVGR for icons (with the exception of rarely used large static icon images that can be in public directory for performance). SVGR is a project that convert svg files to react component (in a template).

This make development easier. svg format is common among designers and design tools - so I can just copy my asset code to svg and write it in the project directly. Also - all modern IDEs have plugins to view the svg file assets. SVGR have many options to configure the final react component output and it includes SVGO tool to optimize the svg properties.

For next.js project I config svgr in next.config.js

webpack(config) {
    // @see https://react-svgr.com/docs/next/
    config.module.rules.push({
      test: /\.svg$/,
      issuer: { and: [/\.(js|ts|md)x?$/] },
      use: [
        {
          loader: "@svgr/webpack",
          options: {
            dimensions: false,
            // import { ReactComponent as IconName } from './icon.svg';
            exportType: "named",
          },
        },
      ],
    });

and then I create a svgr.d.ts file with

// Typings for SVGR svg icons
declare module "*.svg" {
  import React from "react";
  export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
}

that I include in my tsconfig.json

I use my icons component like

import { ReactComponent as PlusIcon } from "@/assets/icons/plus.svg";

And render them like

<PlusIcon height={16} width={16} />

Single color

When I have icons with single colors - I usually want to color them dynamicall instead of creating the same icon in 2 different colors. This can make the code shorter and easier to understand - and it’s better for performance.

Most of the times - I change the fill or stroke properties for coloring

I change their color to currentColor instead of the single color value.

<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
    <path fill="currentColor" d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</svg>
 
The currentcolor  keyword represents the value of an element's color  property. This lets you use the color  value on properties that do not receive it by default.
 

So the color is the same as the color property. The color property is inherit in CSS - so our icon color will match the closest parent in the DOM that has the color property.

This has many advantages - we sometimes have a parent component that it’s inner icon need to change color when hovering on the parent component. So the trick is to change the parent color or create a css selector for parent with :hover that target one of the icons parents and change their color.

If you are using tailwind this can be done with adding “group” (or named group with tailwindcss-labeled-groups if the parent can render dynamic children) and the icon can have “group-hover:text-gray-200” class. No JS is required in this case and also svg color is transitionable (we can add “transition” class or specify CSS transition property to color).

 

Multiple Colors

Sometimes we want to color an icon with more than one color. It’s more common to have icons whose svg includes gradient colors - so we have start and stop colors. Since we cannot use 2 currentColor values - we need to find another way. I do it with CSS variables.

<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <path fill="url(#my-gradient)" d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
  <defs>
    <linearGradient
            id="my-gradient"
            x1="15.4898"
            y1="13.2356"
            x2="16.6025"
            y2="20.759"
            gradientUnits="userSpaceOnUse"
    >
      <stop stop-color="var(--my-color-1)" />
      <stop offset="1" stop-color="var(--my-color-2)" />
    </linearGradient>
  </defs>
</svg>
 

Now you need to init those colors in :root selector. Or if you don’t want - you can change the SVG file to have a fallback like var(—my-color, currentColor) for example. I then change the CSS variable in react with CSS class that I declare like

.active-element {
    --my-color-1: theme("colors.gray.200");
    --my-color-2: theme("colors.gray.300");
  }

of I can use the tailwind arbitrary properties feature to change the css variables to be like [--my-color-1:red] [--my-color-2:blue] or if we use our design system we can change them to [--my-color-1:theme(colors.gray.200)] [--my-color-2:theme(colors.gray.300)]

 

Opacity colors

I try to avoid opacity colors as much as I can. Opacity colors can make problems because their color looks different over different background colors layers. When the background layer color changes - it can affect the final color and I have to check it too. No opacity colors and no opacity property (which can result in more damage because it can affect larger scope without the ability to stop it for sub part of the DOM).

 

When I got an svg with opacity as one of it’s svg properties / have color with opacity - if the icon can be converted to a solid opaque color - I do it / ask the designer to do it. There are great tooks to determine the calculated opaque color that is a result of an opacity color and a background. I use https://filosophy.org/code/online-tool-to-lighten-color-without-alpha-channel/ to do it.

 

But sometimes the icon has multiple shaeds of the same color with different opacity. I prefer to have only single color (from the design system) that change it’s opacity correctly instead of having multiple opaque that made of the opacity color and the layer background. So I duplicate the svg element parts that had an opacity. Then I modify their copy only - delete the opacity and replace their fill / stroke to the background layer color (99% of the times it’s white). This way I can be sure my icon has an opaque layer in the opacity parts - and we can avoid the opacity problem.

 
 

Removing svg filter ids

Then I had a new problem

This caused because the svg contain

<svg
  width="25"
  height="25"
  viewBox="0 0 25 25"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
>
  <path
    d="M18.0025 8.59198L13.859 4.69208C13.5515 4.41744 13.3392 4.19772 12.9215 4.19772C12.5039 4.19772 12.2916 4.41744 11.9841 4.69208L7.84059 8.59198C7.37749 9.00416 7.29688 9.18257 7.29688 9.47084C7.29688 9.96519 7.71685 10.3497 8.23432 10.3497H10.1092V16.5017C10.1092 16.9872 10.5287 17.3805 11.0466 17.3805H14.7964C15.3143 17.3805 15.7339 16.9872 15.7339 16.5017V10.3497H17.6087C18.1262 10.3497 18.5462 9.96519 18.5462 9.47084C18.5462 9.18257 18.4656 9.00416 18.0025 8.59198Z"
    fill="url(#paint0_linear_1704_33112)"
  />
  <rect
    x="5.31641"
    y="19.4568"
    width="15.2149"
    height="1.70029"
    rx="0.850146"
    fill="url(#paint1_linear_1704_33112)"
  />
  <defs>
    <linearGradient
      id="paint0_linear_1704_33112"
      x1="12.9215"
      y1="4.19772"
      x2="20.029"
      y2="17.1601"
      gradientUnits="userSpaceOnUse"
    >
      <stop stop-color="var(--view-actions-color-1)" />
      <stop offset="1" stop-color="var(--view-actions-color-2)" />
    </linearGradient>
    <linearGradient
      id="paint1_linear_1704_33112"
      x1="12.9238"
      y1="19.4568"
      x2="13.0372"
      y2="21.6254"
      gradientUnits="userSpaceOnUse"
    >
      <stop stop-color="var(--view-actions-color-1)" />
      <stop offset="1" stop-color="var(--view-actions-color-2)" />
    </linearGradient>
  </defs>
</svg>

And use the same filter id for both icons.

To fix it - I need to have unique ids for each react component generated by svgr

See https://www.antonball.dev/blog/2020-06-15-svg-id-collision/ for more details about the problem.

I found

Portrait photo of me
Nir Tamir
Developer
@NirTamir
© 2022 Nir Tamir