jahed.dev

Migrating to Tabler Icons in React with SVGR

As design trends change over the years, so does iconography. New symbols come to represent new actions and their styles change alongside human interfaces.

FrontierNav uses FontAwesome for its icons. It's been the most popular choice for at least a decade now and the free tier has served me well over the years for small free projects. However, it's been slowly declining as new symbols are introduced in its paid tiers and its free tier stagnates. Not having the icon I need, or not having a simple variant of an icon just becomes frustrating. I could upgrade to their Pro subscription, but for side-projects, not being tied to an annual subscription is a must for me. There are times I don't work on side-projects for a year or more but I still expect it to be available without needing to pay yet another bill.

Tabler Icons in a way is what FontAwesome used to be many years ago. It's a free and open source set of thousands of icons with no strings attached. Updates are regular and for the most part any icon I can think of is available. The icons aren't as well made as FontAwesome, but that's not a fair comparison considering their differing goals.

Flipping the switch

Given I'm using so many FontAwesome icons, how do I go about migrating them to Tabler Icons? Luckily, I knew this day would come so I added a few abstractions to make migrating easier.

All of FrontierNav's React components use an <Icon /> component.

<Icon type="entity-type-folder" />

The type prop is used to lookup a FontAwesome icon. So rather than importing and hardcoding FontAwesome icons all over the codebase, they're defined once in a single mapping.

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

export const faIcons = {
  "entity-type-folder": (props) => <FontAwesomeIcon icon="folder" {...props} />,
};

Now switching to Tabler Icons is as simple as creating another mapping module.

import { IconFolders } from "@tabler/icons/icons-react";

const tablerIcons = {
  "entity-type-folder": IconFolder,
};

To allow for gradual migrations, we can lookup the icon in both maps.

const Icon = tablerIcons[type] || faIcons[type];

Simple enough. There are a few issues though that make this migration a bit more complicated.

Tabler Icons TypeScript Support

Tabler Icons come with React components. However, the types for these components are wrong; as seen in this issue. In general I've found FontAwesome's React components to be unnecessarily fiddly too so I don't want to make the same mistake again.

SVG in React

Icons nowadays stopped using font hacks and now use SVG. HTML supports SVG elements and so does React. So we can use the SVG files directly. I could manually copy SVG content into React components, or I can use SVGR instead which does this on the fly at compile time.

Currently in my Webpack builds, SVG files are included as separate files. I still need this for some files since not all SVGs are used in a React context. For example, some are used in CSS. So I used Webpack's resourceQuery feature to differentiate those contexts.

{
  /**
   * Convert SVG imports into React components.
   */
  test: /\.svg$/,
  issuer: /\.(js|jsx|ts|tsx)$/,
  resourceQuery: /react/, // *.svg?react
  use: [
    loaderConfig.babel,
    {
      loader: '@svgr/webpack',
      options: {
        babel: false,
        prettier: false,
        svgo: false, // Breaks fill attributes
        jsxRuntime: 'automatic',
        icon: true // Use 1em instead of fixed width/height
      }
    }
  ]
}

Now I can change my imports to use SVGR.

import IconFolder from "@tabler/icons/folder.svg?react";

const tablerIcons = {
  "entity-type-folder": IconFolder,
};

Note, while Tabler's icons are under node_modules/@tabler/icons/icons/folder.svg (two icons directories), it has import remappings in its package.json which squashes them into one directory and doesn't let you access them via normal filesystem paths.

Style Differences

As I mentioned before, generally FontAwesome's icons are of higher quality. One of those quality differences is how they fit into their viewbox and scale to different sizes.

FontAwesome fits edge-to-edge to ensure icons are consistently sized. Tabler Icons have whitespace all around and use a fixed square aspect ratio. Luckily, I used the fixedWidth option with all of my FontAwesome icons so fixed aspect ratio isn't an issue. However, Tabler Icons are smaller as they're not edge-to-edge to their viewbox. So we need to make a few minor CSS adjustments.

.Icon--tabler {
  display: inline-block;
  vertical-align: text-bottom;
  font-size: 1.25em;
  stroke-width: 7%;
}

And that's pretty much everything.

Conclusion

Planning ahead and knowing there'd come a point where I'd want to change icons saved me a lot of time here. It's a good rule to follow: wrap your third-party dependencies in your own abstractions so you can plug in something else in the future.

In general, I like Tabler Icons more than FontAwesome's free tier. The icons feel more sleek and compact and after migrating off FontAwesome's React library, it's a lot simpler too.

It's nice to just search for an icon and have it be available. I no longer have to deal with the fact that while the chevron-left is free, the one with a vertical line on the left isn't.

If you're looking for a default icon set for your projects, I highly recommend Tabler Icons.

Thanks for reading.