paint-brush
How to Import Icons From Figma in a Cool Wayby@smileek
339 reads
339 reads

How to Import Icons From Figma in a Cool Way

by Andrei SieeduginMarch 4th, 2025
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

This guide shows how to easily import icons from Figma into a React/Vue project, and create a component that allows to use them in different sizes and colors.
featured image - How to Import Icons From Figma in a Cool Way
Andrei Sieedugin HackerNoon profile picture

Say you have a set of icons in Figma and a project in React/Vue/whatever else. No surprise, you will need to use those icons. How can we import them?


If you're stubborn enough, you could download them one by one and add them as assets. But that’s a sad way. No likey.


Let’s stop being sad and be awesome instead. Here’s what we’ll need:

  • A nice component that allows using those icons in different sizes and colors.
  • Autocomplete by icon names.
  • Simple update when a new icon is added.


*no further ado provided*

Start Here

We’ll need a Figma token. I don’t think it’s necessary to explain how to do this, so here’s a help section instead: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens.


The default settings are good enough: we need read-only access to the file content.



Give Me Your ID

I’ll use Material Design Icons (Community) as an example.


We’ll need a file key and the main node id:


Now that we have enough prerequisites, let’s start coding. I like my scripts separated from the main project code, so I’ll create a scripts folder in the root and a fetch-icons.ts in it. There’s no need to make it in TypeScript, but it makes this article kinda fancy, so I’ll go with it.


However, this will require a couple more dependencies:

yarn add -D tsx dotenv axios @figma/rest-api-spec


dotenv is here to use the .env variables, tsx — to run the TS script (fancy, remember?), @figma/rest-api-spec is for type safety, and axios is just because I’m used to it. The only one required is dotenv (unless you’re ok with adding the API key to the code), and I’ll add the “pure” version at the end. For now, we will have this setup, and here’s what we start with:

import * as dotenv from "dotenv";

dotenv.config();

const PERSONAL_ACCESS_TOKEN = String(
  process.env.VITE_FIGMA_PERSONAL_ACCESS_TOKEN
); // I’ve added my Figma token to the .env file, and so should you

const FIGMA_API_URL = "https://api.figma.com/v1";
const FILE_KEY = "v50KJO82W9bBJUppE8intT"; // this one is from the URL
const NODE_ID = "2402-2207"; // node-id query param, also from the URL


Fetch!

At first, we’ll get the metadata for the main frame:

import {
  GetFileNodesResponse,
  GetImagesResponse,
  HasChildrenTrait,
} from "@figma/rest-api-spec";

// ...



const fetchFrameData = async (
  nodeId: string
): Promise<HasChildrenTrait> => {
  const { data } = await axios.get<GetFileNodesResponse>(
    `${FIGMA_API_URL}/files/${FILE_KEY}/nodes?ids=${nodeId}`,
    getConfig()
  );

  return data.nodes[nodeId].document as HasChildrenTrait;
};


It will return the information about its children. Those can be images (their type will be INSTANCE) or other frames that we will parse recursively:

const getImageNodes = (
  frameData: HasChildrenTrait
): Record<string, string> => {
  const nodes: Record<string, string> = {};

  for (const image of frameData.children.filter(
    (node) => node.type === 'INSTANCE'
  )) {
    // normalizeName simply converts 'check-box' to 'CheckBox'
    const name = normalizeName(image.name);
    const id = image.id;
    nodes[id] = name;
  }

  for (const frame of frameData.children.filter(
    (node) => node.type === 'FRAME'
  )) {
    Object.assign(nodes, getImageNodes(frame));
  }

  return nodes;
};


Finally, when we have a bunch of image IDs, we will fetch SVG file URLs:

const fetchImageUrls = async (
  nodeIds: string[]
): Promise<GetImagesResponse> => {
  const { data } = await axios.get<GetImagesResponse>(
    `${FIGMA_API_URL}/images/${FILE_KEY}?ids=${nodeIds.join(",")}&format=svg`,
    getConfig()
  );

  return data;
};


And we will end up with an array of such objects:

{
  name: 'CheckBox',
  url: 'https://figma-alpha-api.s3.us-west-2.amazonaws.com/images/7dd5bc31-4f26-4701-b155-7bf7dea45824'
}


As you’ve already guessed, we’ll download them all, but that’s not the most interesting part.

Their Anatomy

Every time we fetch an SVG, we get something like that:

<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M17 6H7C3.69 6 1 8.69 [...] 9 17 9Z" fill="black" />
</svg>


We will remove the svg tags since they are identical for each icon (I assume they are the same size for simplicity). But what’s more interesting is that we’ll replace all the fill=”black” with a placeholder in a template literal, and as a result, we’ll have such a function:

export const IconToggleOn = (color: string) =>
  `<path d="M17 6H7C3.69 [...] 9 17 9Z" fill="${color}"/>`;


Here’s how we get it:

const cleanupSvg = (data: string): string => {
  const svgTags = /<\/?svg.*?>/g;
  const colorFills = /fill=["'].+?["']/g;

  return data
    .replace(svgTags, "")
    .replace(colorFills, 'fill="${color}"')
    .replace(/\n/g, "");
};


Obviously, you won’t need to replace the colors if you need to import some logos, but making a different logic for them will be your homework.

Let’s Get It Started

Okay, here’s our plan:

  1. Remove the resulting file if it exists.
  2. Fetch icon nodes from Figma.
  3. Fetch icon URLs for those nodes.
  4. Download SVGs.
  5. Create a new file with our icon functions.


I won’t recite the whole file—you can check it on GitHub. Let’s just take a look at the downloading itself:

const FILE_BATCH_SIZE = 10;

const fileContent: string[] = [];

const getPromise = async (item: IconData) => {
  try {
    const { data } = await axios.get<string>(
      item.url,
      getConfig("image/svg+xml") // returns necessary options and headers
    );
    return { name: item.name, data };
  } catch (err) {
    console.error(err);
  }
};

for (let i = 0; i < iconsData.length; i += FILE_BATCH_SIZE) {
  const batch = await Promise.allSettled(
    iconsData.slice(i, i + FILE_BATCH_SIZE).map(getPromise)
  );

  for (const icon of batch) {
    if (icon?.status === "fulfilled" && icon.value) {
      const iconName = `Icon${icon.value.name}`;
      const svgData = cleanupSvg(icon.value.data);
      fileContent.push(
        `export const ${iconName} = (color: string) =>\n  \`${svgData}\`;\n`
      );
    }
  }
}


Batching is important here because otherwise, Figma won’t like you if you decide to download a couple of thousands of files in parallel.


If you need to get an icon by the name you receive from the backend, you can also create a dictionary:

const iconArray: string[] = [];
// ...

  for (const icon of batch) {
    if (icon?.status === "fulfilled" && icon.value) {
      const iconName = `Icon${icon.value.name}`;
      const svgData = cleanupSvg(icon.value.data);
      fileContent.push(
        `export const ${iconName} = (color: string) =>\n  \`${svgData}\`;\n`
      );
      iconArray.push(iconName); // <-- we added this 
    }
  }

// ... when all files are processed 
fileContent.push(
  `\r\nexport const iconDictionary: Record<string, (color: string) => string> = {\r\n  ${iconArray.join(
    ",\r\n  "
  )}\r\n};\r\n`
);


This approach is worse since the whole set will be added to the bundle even if you use just one icon via dictionary, but sometimes, you have to make something unpleasant. Also, all the \r\n and spaces are here to make my ESLint happy: you might need to adjust their amount.


Here comes the last step; let’s make a new file:

import path from "path";
import { fileURLToPath } from "url";

// ...
const PATH_TO_ICONS = "../src/assets/icons";

const currentPath = fileURLToPath(import.meta.url);
const currentDirectory = path.dirname(currentPath);

const fileName = "index.ts";

const filePath = path.join(
  path.resolve(currentDirectory, PATH_TO_ICONS),
  fileName
);

// ...
fs.writeFileSync(filePath, fileContent.join(""));


One Little Thing

It’s a good idea to sort icons after fetching the URLs, which is as simple as this:

iconsData.sort((a, b) => a.name.localeCompare(b.name));


A sorted list will be easier to read, and, what’s more important, we’ll have a readable diff whenever we get new icons. Otherwise, the order is not guaranteed, and adding one icon can mess everything up.

We recreate the file, but the order of the icons remains intact


Now What?

We need the component that accepts the icon string, color, and size as props and draws something sweet:

import React, { useMemo } from "react";
import { IconSvg } from "../types/IconSvg";

interface IconProps {
  svg: IconSvg; // type IconSvg = (color: string) => string;
  color?: string;
  size?: number | string;
}

const refine = (x: string | number) => {
  return typeof x === "number" || !/\D+/.test(x) ? `${x}px` : x;
};

const MyIcon: React.FC<IconProps> = ({
  svg,
  color,
  size,
}) => {
  const iconColor = useMemo(() => color ?? "black", [color]);

  const refinedSize = useMemo(() => refine(size ?? 24), [size]);

  const currentIcon = useMemo(() => svg(currentColor), [svg, currentColor]);

  return (
    <svg
      viewBox="0 0 24 24" // make sure it's the same as in Figma
      dangerouslySetInnerHTML={{ __html: currentIcon }}
      style={{
        width: refinedSize,
        minWidth: refinedSize,
        height: refinedSize,
        minHeight: refinedSize,
      }}
    ></svg>
  );
};

export default MyIcon;


I believe it’s pretty straightforward, except for the size refining: when I implemented it for the first time, Chrome accepted numbers as width/height CSS properties, but Firefox didn’t, so I added px. Even if it’s not a case anymore, let’s keep it as it’s supposed to be according to standards.


And here’s the autocomplete that I promised:

That’s All Folks

This is how I cook it. I hope you find it useful.


The drawback of this approach is that the icons will be inlined and not cached because of that. However, I don’t think it’s a big deal for a few bytes. Moreover, you can add rotating, changing the color on hovering, whatever else, and you’ll have a tiny Swiss-army knife of those icons.


Also, it’ll require an organized file in Figma. But hey, if you can write all this code, you can also persuade your designers to sort it out.


You can find the full code, including React and Vue components, and a Node.js version with fewer dependencies here: https://github.com/Smileek/fetch-icons