DEV Community

Cover image for Create your own React icons library and publish to NPM automatically
Quan Pham
Quan Pham

Posted on • Edited on

Create your own React icons library and publish to NPM automatically

Cover photo by Harpal Singh on Unsplash

You have a set of SVG icons in your React project. And you want to separate them out from your current codebase, make them a standalone library so you can reuse these icons later in another projects. This tutorial of mine can help you easily create your own icon library and automatically publish it to the npm.

Fact: I’ve learnt this from watching tailwindlabs/heroicons

TLDR: If you want to skip this post and want to check the final work instead, please click here react-icon-boilerplate. Feel free to clone my repo and create you own lib.

Setup

First, you create an empty folder and initialize a new package.json file.



mkdir react-icons-boilerplate && cd react-icons-boilerplate
yarn init -y
yarn add -D svgo rimraf


Enter fullscreen mode Exit fullscreen mode

We will use svgo to optimize our SVG icon files since:

SVG files, especially those exported from various editors, usually contain a lot of redundant and useless information. This can include editor metadata, comments, hidden elements, default or non-optimal values and other stuff that can be safely removed or converted without affecting the SVG rendering result.

You create a raw folder which contains all your SVG icon files that need to be optimized.



mkdir raw


Enter fullscreen mode Exit fullscreen mode

This is my SVG sample file, I will place it in the raw folder raw/plus-outline.svg



<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>CC3942F2-90B2-4E94-AADC-715CECF64617</title>
    <defs>
        <rect id="path-1" x="0" y="0" width="24" height="24"></rect>
    </defs>
    <g id="200720" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="TP.io---Documentation---Icons" transform="translate(-444.000000, -181.000000)">
            <g id="Small---24-x-24px" transform="translate(208.000000, 141.000000)">
                <g id="Icons/Guideline-Preview" transform="translate(176.000000, 0.000000)">
                    <g id="Add" transform="translate(60.000000, 40.000000)">
                        <mask id="mask-2" fill="white">
                            <use xlink:href="#path-1"></use>
                        </mask>
                        <use id="Mask" fill-opacity="0" fill="#FFFFFF" xlink:href="#path-1"></use>
                        <path d="M12,2.25 C17.3847763,2.25 21.75,6.61522369 21.75,12 C21.75,17.3847763 17.3847763,21.75 12,21.75 C6.61522369,21.75 2.25,17.3847763 2.25,12 C2.25,6.61522369 6.61522369,2.25 12,2.25 Z M12,3.75 C7.44365081,3.75 3.75,7.44365081 3.75,12 C3.75,16.5563492 7.44365081,20.25 12,20.25 C16.5563492,20.25 20.25,16.5563492 20.25,12 C20.25,7.44365081 16.5563492,3.75 12,3.75 Z M12.75,7.46052632 L12.75,11.249 L16.5394737,11.25 L16.5394737,12.75 L12.75,12.749 L12.75,16.5394737 L11.25,16.5394737 L11.25,12.749 L7.46052632,12.75 L7.46052632,11.25 L11.25,11.249 L11.25,7.46052632 L12.75,7.46052632 Z" id="Combined-Shape" fill="#00497A" mask="url(#mask-2)"></path>
                    </g>
                </g>
            </g>
        </g>
    </g>
</svg>


Enter fullscreen mode Exit fullscreen mode

It does looks really long and noisy. Let’s optimize it!

Optimize SVG files

To easily run svgo command with some options, you add a script into the package.json like below. Every time you run the command, it will re-create a folder named optimized which contains all icons that are optimized from the raw folder.



{
  "name": "react-icons-boilerplate",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "optimize": "rimraf ./optimized & svgo -q -p 8 -f ./raw -o ./optimized"
  },
  "devDependencies": {
    "rimraf": "^3.0.2",
    "svgo": "^2.6.1"
  }
}


Enter fullscreen mode Exit fullscreen mode

You can run yarn optimize to see the result. Also, you need to have a svgo configuration file in order to add as many plugins as possible that help us to clear all the redundant data. Here is my svgo.config.js for example:



module.exports = {
  multipass: true,
  js2svg: {
    indent: 2,
    pretty: true,
  },
  plugins: [
    { name: 'preset-default' },
    'sortAttrs',
    'removeScriptElement',
    'removeDimensions',
    'removeScriptElement',
    'removeDimensions',
  ],
};


Enter fullscreen mode Exit fullscreen mode

Before come up with these plugins, I tried to mess around on this website SVGOMG - SVGO’s Missing GUI

This is my optimized result file.

Build React icons

We need some packages in order to transform our SVG files into React SVG components and then convert JSX syntax into CJS and ESM module format. We use babel and svgr.



yarn add -D @babel/core @babel/preset-react @svgr/cli @svgr/core camelcase terser


Enter fullscreen mode Exit fullscreen mode

You get this build script from here: script/build.js. Basically, this script read the optimize folder, get all icons and convert them into JSX by using svgr and then transform the React code into CJS and ESM module format by babel.

The original icon filename will become the component’s name in CamelCase plus suffix Icon. For example with plus-outline.svg file we will have component name PlusOutlineIcon.

The package supports for Typescript by generate a declare file .d.ts for each icons.

This is the usage of the icon component when the packages is installed and used by other project



import { PlusOutlineIcon } from 'react-icons-boilerplate'


Enter fullscreen mode Exit fullscreen mode

Since this build script I created to fit my icons set so these line are little special, but you can change them to fit yours icons set.



// line 12 to 23
const svgReactContent = await svgr(
  content,
  {
    icon: false,
    replaceAttrValues: { '#00497A': "{props.color || '#00497A'}" },
    svgProps: {
      width: 24,
      height: 24,
    },
  },
  { componentName }
);


Enter fullscreen mode Exit fullscreen mode

Now, we add this build script to our package.json



...
"scripts": {
    "optimize": "rimraf ./optimized & svgo -q -p 8 -f ./raw -o ./optimized",
    "build": "yarn optimize && node scripts/build.js"
},
...


Enter fullscreen mode Exit fullscreen mode

If you run yarn build you will have a dist folder as the result. You see something similar to this

3539DB0D-179B-46AA-9BB7-2AD78351EB80

And don’t forget to add these lines in your package.json before publishing it.



{
  // ...
  "main": "./dist/cjs/index.js",
  "module": "./dist/esm/index.js",
  "files": [
    "dist"
  ],
  // ...
}


Enter fullscreen mode Exit fullscreen mode

Git

Now you can push all your work to your GitHub repository. I won’t go through this since I assume we all know how to do it.

Publish

When you have the final dist folder as the result now you can publish this to npm.
It can be easily done by login into the npm account via NPM CLI npm login and run the npm publish --access public after that. But I want to leverage GitHub Actions and atlassian/changesets to do this automatically for me.

changesets

To install and init changesets:



yarn add -D @changesets/cli @changesets/changelog-github
yarn changeset init


Enter fullscreen mode Exit fullscreen mode

You will have .changeset folder and its configuration file config.json inside. Here is my config:



{
  "$schema": "https://unpkg.com/@changesets/config@1.6.1/schema.json",
  "changelog": [
    "@changesets/changelog-github",
    { "repo": "mikunpham/react-icon-example" }
  ],
  "commit": false,
  "linked": [],
  "access": "restrict",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}


Enter fullscreen mode Exit fullscreen mode

If you want to publish your package as a public package, you have to change the “access” property from restricted to public.

For the first release, you have to run yarn changeset. It will ask what kind of Semantic Versioning you want to bump your package and write a short summary about it. It will create something like this and you now can commit this file to your repository.

86BDEFE4-2AF0-4599-84DF-58E441E81FCD

GitHub Release Action

changesets has a very detailed instruction to implement their action into your GitHub workflows here https://github.com/changesets/action
Or you can get my release.yml here release.yml.

Now, commit everything and push onto Github.

If everything is OK, a github-action bot will create a PR just like this and wait for you to merge.

45043714-16A2-4DA1-B4F4-C07276FA1885

After merging, a release will be created

22E81BD3-5302-465D-A6E0-BFA0756296EF

And go check your npm now.

8BAF8198-D4BE-4882-B2F7-66EE9973889D

What’s next

From now, whenever you have new icons, do these following step.

  1. Add them to the raw folder.
  2. yarn build
  3. yarn changeset , select major/minor/patch bump and write summary.
  4. Commit
  5. Profit!

Final

A very long long post huh? BUT it just takes you 30’ max to get everything up and run for the first time and mostly 2-3’ to add new icons and release new version after that.

Thank you all the contributors of babel, svgo, svgr and changesets for making our dev life easier than ever.

597AFCAB-C5DC-49E1-AF5A-D68232397823

Thank you for reading till the end 🎉

Top comments (6)

Collapse
 
kriptonian profile image
Sawan Bhattacharya

if you are getting error rimraf Error: invalid rimraf options. in build.js then change the

new Promise((resolve) => {
    rimraf(`${outputPath}/*`, resolve);
  })
    .then(() => Promise.all([buildIcons('cjs'), buildIcons('esm')]))
    .then(() => console.log('✅ Finished building package.'));
Enter fullscreen mode Exit fullscreen mode

to this

rimraf(`${outputPath}/*`)
    .then(() => Promise.all([buildIcons("cjs"), buildIcons("esm")]))
    .then(() => console.log("✅ Finished building package."))
    .catch((error) => {
      console.error("❌ Error building package:", error);
      exit(1);
    });
Enter fullscreen mode Exit fullscreen mode

here is the documentation to it link

Collapse
 
minhthangtkqn profile image
Hoàng Minh Thắng • Edited

Hi. Thanks for sharing it. @quanpham
I followed your guide and built my own library.
But now I encounter a problem: I export my own icon from Figma to svg, then use this svg file to make component. But I cannot apply other color to the result Icon component.
Can you tell me which tool that you use to create the svg file in the boilerplate?
Thank you.

Collapse
 
quanpham profile image
Quan Pham

Hey @minhthangtkqn Thank you for reading through this.

The SVGs that I used were exported from Invision which made by our designers.

In your case, I think you can modify the build script a bit to fit your icon set.
Link here

Mine icons always contain color #00497A which is the main color of the set. I replaced it with props.color so it will let me customize the color later.

// example
<IconName color="red" />
Enter fullscreen mode Exit fullscreen mode

If your icon set is way more complex than mine, then you can read SVGR document to see more detail configurations to apply.

Collapse
 
gchumillas profile image
Gonzalo Chumillas

It's cool, man! thanks for sharing it.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
gnato profile image
MikolajGn

hey, I have found fork here: github.com/doubleppereira/react-ic...