Automating SVG Icon Generation

I’m quite proud of this week’s achievement: I automated the process of updating icons in a React app from a Figma file. I’m sharing the details for my future self and whoever might find it useful.

The Problem

The app I’m working on has a lot of icons in SVG format. The icons are scattered all over the codebase and are not consistent, due to the way we work: multiple independent teams, working on a very lean way, without a dedicated design system team.

Maintaining the icons is a pain. Every time we need to update an icon, we have to:

This process is error-prone, time-consuming, and needs to be done by a developer. Designers don’t own the process, and they can’t update the icons themselves.

Until now.

The Solution

Step 1: Unify the Icons

I started by creating a single Icon component in React, to be used throughout the app. This component has a name prop to pick the icon to display. Thanks to TypeScript types, the name is autocompleted in most code editors, reducing errors while consuming the icons. It also has a size prop to choose from a predefined list of sizes and a color prop to set the color, with sane defaults.

Storybook screenshot showing the Icon component

The way the Icon component works is simple: it imports all the icons from an index file and then picks the right one based on the name prop. This way, we can easily add new icons by adding them to the file, without having to change the Icon component.

import * as iconIndex from "../../icons";

type IconProps = {
  name: keyof typeof icons;
  size?: 14 | 32 | 64;
  color?: string;
};

export const Icon = ({ name, size = 14, color = "white" }: IconProps) => {
  const SvgIcon = iconIndex[name];
  return (
    <SvgIcon style={{ color: color }} width={size} height={size} aria-hidden />
  );
};

Step 2: Automate the Icon index

The icons folder contains all the icons in SVG format. We use the SVGR package to convert the SVG files to React components (in particular, since we use Vite, we use the vite-plugin-svgr package, more on this later).

The file with all the icons is automatically generated by a script that imports whatever is in the icons folder, and exports them as named exports. This way, we can add new icons to the app by simply adding them to the icons folder. The script can be run manually by developers whenever they need to update the icons, but is also run automatically in the CI/CD pipeline as part of the build process.

import fs from "fs";
import path from "path";

console.log("Updating icons...");

const iconsDir = path.resolve("./src/icons/");

const cleanUpIconName = (name) => {
  return name.replace(".svg", "").replace(/[-.]/g, "_").replace("icon_", "");
};


const svgFiles = fs
  .readdirSync(iconsDir)
  .filter((file) => file.endsWith(".svg"))
  .sort((a, b) => cleanUpIconName(a).localeCompare(cleanUpIconName(b)));

const imports = svgFiles
  .map((file) => {
    const variableName = cleanUpIconName(file);
    return `import ${variableName} from "./${file}?react";`;
  })
  .join("\n");

const exports = svgFiles
  .map((file) => {
    const variableName = cleanUpIconName(file);
    return `export { ${variableName} };`;
  })
  .join("\n");

const content = `${imports}\n\n${exports}\n`;

fs.writeFileSync(path.join(iconsDir, "index.ts"), content);

console.log("Finished updating icons.", svgFiles.length, "icons updated.");

Step 3: Clean up the SVGs with SVGR

The SVGs are not always perfect. Sometimes they have extra attributes, or they are not optimized for the web. We use the svgo plugin to optimize the SVGs before converting them to React components. This is done by the vite-plugin-svgr package, which is a wrapper around SVGR for Vite.

Our vite.config.js file looks like this (simplified):

import svgr from "vite-plugin-svgr";

export default {
  plugins: [
    svgr({
      svgrOptions: {
        svgo: true,
        replaceAttrValues: {
          white: "currentColor"
        }
      }
    })
  ]
};

The key part here is the replaceAttrValues option, which replaces the white color with currentColor. This is important because we want to be able to change the color of the icons using CSS, without having to modify the SVG code itself manually. You can pick whatever color makes sense for you, or even use multiple colors. Our icons are all monochrome, so we only need to replace one color, and white is a good default. But you can experiment with picking an uncommon color, like pink, to make sure you don’t accidentally use it in the SVGs.

This is how the icons look in the Figma file:

Screenshot of the Figma file with the icons

And this is how they look in the app, once converted to React components:

Screenshot of the app with the icons

We use Storybook to showcase the icons, so that developers can easily find the one they need. The icons are displayed in two tones to make sure we’re coloring the right parts of the icon. They also zoom on hover, to make it easier to see the details.

A nice side effect of this process is that it is now easier to detect inconsistencies in the icons. For example, if two icons are supposed to represent the same thing but look different, it’s easier to spot that now that they are all in one place. Or if two icons look the same but have different names, it’s easier to spot that too:

Duplicate icons in the Storybook

Step 4: Automate the export process

This is the most important part, and the one that makes the whole process worth it: we’re using the Figma API to download the icons directly from the Figma file. This way, designers can update the icons in Figma, and the changes will automatically propagate to the app.

To do that, we use the Figma API and the figma-export-icons package, plus a couple of custom scripts to make it work with our setup. Basically, deleting the old icons before exporting the new ones, renaming icons once they’re downloaded, etc. Pretty straightforward stuff.

This step is not run automatically in the CI/CD pipeline, because we still want to have some control over when the icons are updated. But it could be done, if we wanted to.

Future Improvements

This is just the beginning. Here are some ideas for future improvements:

Conclusion

This was a lot of work, but I really enjoyed the process and the end result.

I’m happy that I was able to automate the process of updating icons in the app, and I’m looking forward to seeing how it will improve the workflow of the team.