How to Build a Custom SVG Icon Component Library in React and Next.js

Learn how to build, scale, and publish your own custom SVG icon library for React and Next.js projects using SVGR and optimized workflows.

Amit Yadav
Amit Yadav

Building a custom SVG icon library in React and Next.js requires balancing developer experience, bundle size, and performance. While using generic third-party packs is great for side projects, enterprise applications and comprehensive design systems usually demand a bespoke approach.

In 2026, the standard approach involves converting raw SVG files into highly customizable, type-safe React components using automated tooling, rather than manually writing JSX.

Here is the definitive guide to building a scalable custom SVG icon component library.

Why Build a Custom Library?

Relying on direct SVG imports or manual component creation comes with several drawbacks at scale:

  • Bundle Bloat: Unoptimized SVGs carry metadata, comments, and hidden paths.
  • Inconsistency: Different developers might use different sizing or coloring methods (e.g., fill vs stroke).
  • Maintenance Nightmare: Updating an icon means hunting down every instance of that SVG across your codebase.

A custom component library solves this by centralizing your assets.

Step 1: The Automated Pipeline with SVGR

Do not import raw SVGs directly and manually convert them to JSX. Instead, use SVGR (@svgr/cli or @svgr/webpack) to automate the process. SVGR takes raw .svg files and compiles them into React components.

Pre-build vs Build-time

While you can configure Webpack or Turbopack to transform SVGs on the fly, for a dedicated icon package, it’s better to run SVGR as a pre-build step. This generates actual .tsx files that are easier to type-check and publish.

First, organize your raw SVGs in an assets/icons folder.

Next, install the required packages:

npm install --save-dev @svgr/cli svgo

You can add a script to your package.json to process these files:

"scripts": {
  "generate:icons": "svgr --out-dir src/components/icons --icon --typescript assets/icons"
}

Step 2: Optimizing with SVGO

Before SVGR turns your assets into React components, they must be stripped of unnecessary bloat. SVGR integrates with SVGO by default. You can customize the optimization by adding an .svgo.yml or svgo.config.js file in your project root.

A good baseline configuration removes dimensions and ensures the viewBox is retained:

// svgo.config.js
module.exports = {
  plugins: [
    {
      name: 'preset-default',
      params: {
        overrides: {
          removeViewBox: false,
        },
      },
    },
    'removeDimensions',
  ],
};

This ensures your icons can scale responsively via CSS without hardcoded width and height attributes conflicting.

Step 3: The Universal Icon Wrapper

Once you have generated components (like MenuIcon.tsx and UserIcon.tsx), it’s best practice to expose them through a central Icon Wrapper Component. This enforces consistency across your app.

import React from 'react';
import * as Icons from './icons'; // Your SVGR generated files

export type IconName = keyof typeof Icons;

interface IconProps extends React.SVGProps<SVGSVGElement> {
  name: IconName;
  size?: number | string;
  className?: string;
}

export const Icon = ({ name, size = 24, className, ...props }: IconProps) => {
  const SvgIcon = Icons[name];

  if (!SvgIcon) {
    console.warn(`Icon "\${name}" does not exist in the library.`);
    return null;
  }

  return (
    <SvgIcon 
      width={size} 
      height={size} 
      className={`inline-block fill-current \${className || ''}`} 
      {...props} 
    />
  );
};

Why this pattern works:

  1. Type Safety: The IconName type is automatically derived from the available icons. TypeScript will throw an error if a developer typos an icon name.
  2. Consistent Sizing: Sizing is strictly controlled.
  3. Styling Integration: By applying fill-current (or text-current depending on your CSS framework), the icon automatically inherits the text color of its parent container.

Step 4: Tree-Shaking and Bundle Performance

If you export all your icons from an index.ts (a “barrel” file), you risk bloating the client bundle if tree-shaking isn’t perfectly configured.

In Next.js (especially with the App Router and Turbopack), ensure that your package.json specifies "sideEffects": false. This tells the bundler it’s safe to drop unused exports.

Alternatively, for massive libraries (thousands of icons), instruct developers to import icons directly:

// Better for massive libraries
import MenuIcon from '@/components/icons/MenuIcon';

Step 5: Built-in Accessibility

Icons fall into two categories: Decorative and Functional. Your wrapper should handle both gracefully.

Update your <Icon> wrapper to enforce accessibility:

export const Icon = ({ name, size = 24, 'aria-label': ariaLabel, className, ...props }: IconProps) => {
  const SvgIcon = Icons[name];
  // ...
  
  const accessibilityProps = ariaLabel 
    ? { 'aria-label': ariaLabel, role: 'img' }
    : { 'aria-hidden': true };

  return (
    <SvgIcon 
      width={size} 
      height={size} 
      {...accessibilityProps}
      {...props} 
    />
  );
};

If a developer passes an aria-label, the icon is treated as meaningful. If not, it’s explicitly hidden from screen readers, preventing clutter.

Conclusion

Building a custom SVG icon component library is a straightforward process that yields massive dividends for code quality and maintainability. By utilizing SVGO for compression, SVGR for automated component generation, and a centralized wrapper for enforcement, your React and Next.js applications will remain fast, accessible, and visually consistent.

Share this post