Joonas' blog

Opinionated UI: SVG icons in React

published

This is the best way I have found to store and serve SVG icons for use in a React design system.

It prioritizes few features:

  • Make it extremely fast to add new icons (as .svg) with minimal manual intervention
  • Serve ready-to-use React components
  • Easily adapt to surrounding font size and text color

Sourcing the icons

While not directly related to storing and serving the icons, sourcing icons to use is a topic of its own. I like Feather icons, while occasionally getting some icons from Lucide, a Feather icon fork. Of course, sometimes custom app-specific icons are needed as well, but this system for adding icons makes mostly all types of .svg icons easy to add.

Design system structure

Rest of the article assumes the following file structure:

my-ui-system/
- icons/ # all .svg icons are initially placed here
- src/icons/ # generated icon components go here
- src/index.ts
- package.json

Build icons with svgr

SVGR is an excellent tool and just the right choice for this purpose: it handles optimizing the svg, figuring out a nice component name from file name, and has bunch of customization options.

For our purposes, few options are especially useful:

  • --typescript so that we obtain typed Typescript components
  • --icon to replace SVG dimensions (width/height) with 1em to have the icon automatically be sized according to text size
  • (optionally) --replace-attr-values stroke=currentColor to always have stroke color equal currentColor (=icon color inherits from text color). Not needed for feather icons, so not specified here.

This is what the build script to convert all icons/*.svg into src/icons/*.tsx looks like:

"scripts": {
"build:svgr": "npx @svgr/cli --out-dir src/icons --jsx-runtime automatic --typescript --icon -- icons",
},

Export icons

For exporting the icons from the design component library, I prefer using a star export:

// src/index.ts
export * as icons from "./icons";

This ensures that you don’t need to manually add any exports when adding new icons. Additionally, the process of using a newly added icon is then quite simple:

import { icons } from "my-ui-system";
function Alert() {
return <div className="text-orange-500"><icons.AlertTriangle className="mr-2"/> warning</div>;
}

Development mode

To make it all even nicer, in development mode I use nodemon to monitor icons folder and automatically build icons on change. This makes the process of adding a new SVG icon take something like three seconds.

"scripts": {
"dev:svgr": "nodemon --watch icons -e svg --exec npm run build:svgr"
},

So what makes this approach good?

This process of adding icons and the result seem quite straightforward, but having the process look like this is based on years of experience working with design systems. Many of them suffer from problems like having the developer do bunch of manual work running icons through SVG optimizers, producing inconsistently behaving icons (e.g. not inhering text color), or outputting .svg files instead of ready-to-use React components. The approach described here doesn’t suffer from any of these issues and has worked very well in production usage.