ezb.sh logo
Back to Home

Using TypeScript to Generate 4,000+ Figma Components

Written by ezb
typescript
figma
frontend
Article thumbnail courtesy of Med Badr Chemmaoui on Unsplash.

Background

Powering the web technology behind Threshold, a product I was the sole frontend engineer for, is a suite of NextJS applications that are built upon the Mantine design system. Following the implicit recommendation from Mantine's documentation, I utilized the Tabler Icon library throughout the development of Threshold. It is a bountiful cache of icons, grouped into categories, in a style that I am quite fond of and have continued using even in my personal work. (You've been seeing them on this very website!)
A regular pattern of mine is to use iconography next to text to help give extra context to what might be an otherwise obscure or confusing piece of copy. I use it often enough to warrant a component - <TextWithIcon> - which accepts a stringified Tabler icon name (e.g., IconMoodSmile) as a prop, and text content as children. It has options for how close the icon should be for the text, and the dynamic icon rendering under-the-hood uses the Tabler icon CDN so the lazy-loading of icons is performant. But enough about the implementation; what's really important for this post is the design process.
When tasked with adding new functionality to Threshold, I first would open up Figma to get my ideas out. Having access to reusable components and Auto Layout in Figma is a huge time-saver for me, especially when working with an off-the-shelf design system like Mantine. Another huge benefit to me was the Tabler Icons Figma plugin, which allows me to simply search for the icon I want, and it plops the icon as a scalable SVG right into the design file. With these two tools available to me, I was able to quickly iterate and deliver high-fidelity mockups that were very easy to convert into implementation details; I mean, the component structure and layout was already there!
There was one major downside to my design flow, that admittedly took me way too long to rectify in hindsight, which was that I never created a reusable Figma component analogous to <TextWithIcon>. Each time I wanted to include an icon adjacent to some text, my process was this:
  • Create a text node
  • Apply the text styles from my Design Library
  • Enter the text I want
  • Open Figma Plugins
  • Find Tabler Icons plugin, launch it
  • Search for the icon I want, select it
  • Manually resize and adjust stroke-width if it didn't match the text size I wanted
  • Inevitably forget I was going need to do this again, and close the Tabler icons plugin
  • Select the pasted Icon's frame, and the text node
  • Press ⌘ + Shift + G to consolidate the two items into a new Frame
  • Enable Auto Layout on that Frame, adjust Auto Layout properties
Phew! For something I do quite often, that's... a lot of steps. It's also a brittle approach - if I received feedback that the design layout was too big, for example, that meant I would need to manually resize the icons and the text by hand. This process annoyed me every time I did it, and what bothered me most was I knew how I could make my life so much easier - just create a reusable "Text with Icon" Figma component. However, this was not as simple as it may seem.

Why I Suffered for So Long

When creating a reusable component in Figma, one can create "Instance Properties", which are basically variables that can be changed for each instance of a component in your design file. Figma offers a couple of different types of Instance Properties:
  • Variant - Figma components can have multiple "variants", controlled by a "Variant" property. Example would be a "Button" component, that has a "Filled" and "Outlined" variant.
  • Text - User-definable strings whose values are injected into text-nodes you specify
  • Boolean - A toggle that allows for nodes within the component to be shown or hidden
  • Instance Swap - Can be thought of as a "slot" for existing Figma components to be plugged into / "swapped"
I feel like I already know your next question.
So wait... if you could configure Figma items and text as properties, why did you suffer for so long?
The root of my issue stems from a limitation with Instance Swap properties. In order to use some Figma item as an Instance Swap, it must be a component, not simply a Frame or a Group or any other item. This component must additionally either in the design file you're working on, or from one of the Libraries you've imported into the file.
Here's the real kicker - to my knowledge, no one had created a Tabler Icons component library in Figma - the only resource I had was the plugin, which generated simple frames containing the SVG data. In order for my "Text With Icon" dreams to be fulfilled, what I really needed was a Library of icon components.
(While writing this article, I've determined I must not have been looking hard enough. There were at least a couple of options for me to choose from, that I must have just missed. Oh well, at least I learned a lot and got to write an article!)
Okay, how hard can that be? You have the plugin right, with all the icons?
Yeah, about that...
Tabler icons has 4,900 icons
I'm a completionist. I need all the icons. I really was crazy enough to believe in simply going through the plugin and pasting each and every icon in to the file. I got about 100 icons deep, and then realized the plugin isn't paginated, which meant I couldn't just go through the icons one-by-one until I reached the end. I had no way to tell if I had included all of the icons, unless I manually went through and double checked. Such a solution would also be an incredible sink of time to keep updated with Tabler as new icons were released.
But I had the itch to fix this. It was time.

Deciding To Do Something

I got to a point where I had enough fiddling with icon sizing and font weights to make it look good. It was time to create this dang component. What I needed was clear - the Tabler Icon Figma Component Library.
Hey... is that Tabler Icon plugin open source?
Oh, it is? AND it's in TypeScript? Interesting...
After a few minutes of digging, I discovered this icons.json file in the source code of the plugin, which had each icon's SVG data (among other data points) corresponding with the icon name. This was the jackpot! I figured if this plugin could insert icons into my design file, I could create my own plugin that iterated over that list of icons and generate me all the icons I needed.
I quickly researched Figma's Plugin API and was elated to see that createNodeFromSvg was a built-in function that accepts SVG strings - exactly what I needed.
I got to work creating my own plugin to generate the component library. I used the fact that the icons.json file includes the information to correlate an icon with its category to create a Frame for each category to help keep the master icon file tidy. For each category, the icons are iterated through and instantiated as a Figma node using that createNodeFromSvg function. I then use the createComponent method to contain the node, finally giving me the icon components I needed. The component is then nested within its category's Frame, and once all icons have been injected into the category Frame, the category Frame is added to the Figma document.
The full code of the plugin is quite simple. The bulk of it is just assigning the properties for each new Figma node to match the specifications I wanted:
1:
import { icons } from "./icons.json"
2:
import { groupBy } from "lodash"
3:
4:
const iconCategories = groupBy(icons, (icon) => icon.category)
5:
6:
const CATEGORY_FRAME_WIDTH = 420
7:
const generateIconCategory = (categoryName: string, iconData: typeof icons, frameX: number) => {
8:
const frame = figma.createFrame()
9:
frame.layoutMode = "HORIZONTAL"
10:
frame.layoutWrap = "WRAP"
11:
frame.layoutSizingVertical = "HUG"
12:
frame.maxWidth = CATEGORY_FRAME_WIDTH
13:
frame.itemSpacing = 20
14:
frame.name = `${categoryName} Icons`
15:
16:
for (const icon of iconData) {
17:
const node = figma.createNodeFromSvg(icon.svg)
18:
node.name = `${icon.name}-svg`
19:
20:
const component = figma.createComponent()
21:
component.name = icon.name
22:
component.layoutSizingHorizontal = "FIXED"
23:
component.layoutSizingVertical = "FIXED"
24:
component.resize(24, 24)
25:
component.layoutMode = "VERTICAL"
26:
component.constraints = {
27:
vertical: "CENTER",
28:
horizontal: "CENTER"
29:
}
30:
31:
component.appendChild(node)
32:
node.layoutSizingHorizontal = "FILL"
33:
node.layoutSizingVertical = "FILL"
34:
35:
frame.appendChild(component)
36:
}
37:
38:
frame.x = frameX
39:
figma.currentPage.appendChild(frame)
40:
}
41:
42:
let currentFrameX = 0
43:
for (const [rawName, categoryIcons] of Object.entries(iconCategories)) {
44:
const categoryName = rawName ?? "Uncategorized"
45:
generateIconCategory(categoryName, categoryIcons, currentFrameX)
46:
currentFrameX += CATEGORY_FRAME_WIDTH + 100
47:
}
48:
49:
// Make sure to close the plugin when you're done. Otherwise the plugin will
50:
// keep running, which shows the cancel button at the bottom of the screen.
51:
figma.closePlugin()

The Result

You can view the file here! The result showcases how insane I was to think I could do this by hand. I rearranged it to fit into this screenshot:
Finished component file
Compare the video from the beginning of the post to this - I can easily choose the icon, its size, and even what side of the text it's on. If I received feedback to make the text larger, it is no longer an arduous multi-step process - I can simply change the values in the inspector panel.
(The one disadvantage compared to the plugin approach, is that Figma doesn't do a great job loading the icon previews. A trade-off I was and am more than willing to make.)

Closing Thoughts

This solution worked well enough for me to continue my duties working on Threshold. Grouping the icons into their respective categories had an additional benefit past keeping the master component file clean. When actually using the TextWithIcon Figma component and overriding the Instance Swap property, Figma handily partitions the results by their parent frame, which means I get the full searching capabilities the OG plugin provided me.
I haven't updated the design file since Tabler v2.44.0 since I haven't needed any of the new icons, but in theory simply running this plugin's code again would give me an updated component library. A downside to the implementation presented in this post is that it is dependent on the icons.json file I found in the Figma plugin's source code. A smarter approach would be to import @tabler/icons package directly, and scrounge through the SVG files it provides and load it from there. Perhaps I'll get to that at some point, but for now, I'm just happy I don't need a 11 step process to put an icon next to some text!