DEV Community

albert nahas
albert nahas

Posted on • Originally published at leandine.hashnode.dev

Building a Type-Safe Icon System in Next.js

Modern web applications thrive on consistency and usability, and iconography sits at the heart of both. Whether you're building a design system, a marketing site, or a dashboard, a robust icon system can make your UI more expressive and maintainable. But as projects grow, icon usage can become a source of headaches: mismatched imports, stray SVG files, and runtime errors when an icon name is mistyped. With Next.js and TypeScript, however, you can create a type-safe icon system that transforms SVG files into auto-completed, easily consumable React components.

Let’s walk through a practical approach to building a scalable, type-safe icon system in a Next.js app, leveraging TypeScript’s static safety and React’s composability.

Why Build a Type-Safe Icon System?

Before diving into implementation, it’s worth clarifying the benefits:

  • Consistency: Centralizes icon usage and styling.
  • Type Safety: Eliminates runtime errors from invalid icon names.
  • Auto-complete: Developers get instant feedback and suggestions when using icons.
  • Theming: Easily support color, size, and accessibility props.

Let’s see how we can achieve this from scratch.

1. Organizing Your SVG Icons

Start by creating a directory for all your SVG files, e.g., icons/ at the project root. Name each SVG file according to its intended component name. For example:

icons/
  arrow-left.svg
  check.svg
  close.svg
  github.svg
Enter fullscreen mode Exit fullscreen mode

Keep your SVGs clean: remove unnecessary attributes (like width, height, and inline fill), use currentColor for fill or stroke to control icon color via CSS, and ensure they’re optimized (with SVGO or similar).

Example of a clean SVG (check.svg):

<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
     stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
  <path d="M5 13l4 4L19 7"/>
</svg>
Enter fullscreen mode Exit fullscreen mode

2. Converting SVGs to React Components

There are several approaches to integrating SVGs as React components in Next.js:

  • Manual conversion: Copy-paste SVGs into .tsx files and wrap them as components.
  • SVGR: Use SVGR to automate conversion of SVGs to React components.
  • Dynamic import: Load SVGs as React components at runtime (with custom webpack rules or plugins).

Using SVGR in Next.js

SVGR is the industry standard for transforming SVG files into React components. With Next.js, you can set up SVGR using custom webpack configuration:

First, install dependencies:

npm install --save-dev @svgr/webpack
Enter fullscreen mode Exit fullscreen mode

Then, update next.config.js:

// next.config.js
module.exports = {
  webpack(config) {
    config.module.rules.push({
      test: /\.svg$/,
      issuer: /\.[jt]sx?$/,
      use: [
        {
          loader: '@svgr/webpack',
          options: {
            icon: true,
            typescript: true,
            prettier: true,
          },
        },
      ],
    });
    return config;
  },
};
Enter fullscreen mode Exit fullscreen mode

Now you can import SVGs as React components:

import CheckIcon from '../icons/check.svg';

export function Example() {
  return <CheckIcon width={24} height={24} />;
}
Enter fullscreen mode Exit fullscreen mode

3. Generating a Type-Safe Icon Registry

To get type safety and auto-completion for icon names, create a registry mapping icon names to components. This enables you to render icons by name, with TypeScript enforcing valid options.

Step 1: Generate an Icon Map

Create a file like icon-map.ts:

import ArrowLeft from '../icons/arrow-left.svg';
import Check from '../icons/check.svg';
import Close from '../icons/close.svg';
import Github from '../icons/github.svg';

export const icons = {
  'arrow-left': ArrowLeft,
  check: Check,
  close: Close,
  github: Github,
} as const;

export type IconName = keyof typeof icons;
Enter fullscreen mode Exit fullscreen mode

Tip: If you have many icons, automate this file's generation with a script that reads the directory and outputs imports and the icons object.

Step 2: Create the Icon Component

Now, create a reusable Icon component that accepts an icon prop, restricted to valid icon names:

// components/Icon.tsx
import { icons, IconName } from '../icon-map';

type IconProps = {
  icon: IconName;
  size?: number | string;
  color?: string;
  className?: string;
  'aria-label'?: string;
};

export function Icon({
  icon,
  size = 24,
  color = 'currentColor',
  className,
  'aria-label': ariaLabel,
}: IconProps) {
  const IconComponent = icons[icon];
  return (
    <IconComponent
      width={size}
      height={size}
      fill={color}
      aria-label={ariaLabel}
      className={className}
      role={ariaLabel ? 'img' : 'presentation'}
      focusable="false"
    />
  );
}
Enter fullscreen mode Exit fullscreen mode
  • The icon prop is type-safe; TypeScript will error if you use an invalid name.
  • Optional props for size, color, and accessibility.

Step 3: Usage with Auto-Completion

Now you can use the Icon component anywhere and get auto-completion for valid icon names:

import { Icon } from './components/Icon';

export function Toolbar() {
  return (
    <div>
      <Icon icon="check" size={20} aria-label="Checkmark" />
      <Icon icon="close" color="#d32f2f" />
      <Icon icon="github" className="social-icon" />
      {/* <Icon icon="not-found" /> // TypeScript error! */}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Automating Icon Imports (Optional)

As your icon collection grows, maintaining icon-map.ts manually becomes tedious. You can automate this with a script. Here’s a basic example using Node.js:

// scripts/generate-icon-map.js
const fs = require('fs');
const path = require('path');

const iconDir = path.join(__dirname, '../icons');
const outFile = path.join(__dirname, '../icon-map.ts');

const files = fs.readdirSync(iconDir).filter(f => f.endsWith('.svg'));

const imports = files
  .map(f => {
    const compName = f
      .replace(/\.svg$/, '')
      .replace(/(^\w|-\w)/g, s => s.replace('-', '').toUpperCase());
    return `import ${compName} from './icons/${f}';`;
  })
  .join('\n');

const iconMap = files
  .map(f => {
    const key = f.replace(/\.svg$/, '');
    const compName = f
      .replace(/\.svg$/, '')
      .replace(/(^\w|-\w)/g, s => s.replace('-', '').toUpperCase());
    return `  '${key}': ${compName},`;
  })
  .join('\n');

const content = `
${imports}

export const icons = {
${iconMap}
} as const;

export type IconName = keyof typeof icons;
`;

fs.writeFileSync(outFile, content);
Enter fullscreen mode Exit fullscreen mode

Add this script to your package.json scripts:

"scripts": {
  "generate:icons": "node scripts/generate-icon-map.js"
}
Enter fullscreen mode Exit fullscreen mode

Every time you add, remove, or rename an SVG, run npm run generate:icons to update your icon registry.

5. Theming and Customization

With all icons using currentColor, you can easily control icon color with CSS or props. For example, to create a styled icon button:

<button className="icon-btn">
  <Icon icon="close" />
</button>

<style jsx>{`
  .icon-btn {
    background: none;
    border: none;
    color: #666;
    padding: 4px;
    cursor: pointer;
    transition: color 0.2s;
  }
  .icon-btn:hover {
    color: #d32f2f;
  }
`}</style>
Enter fullscreen mode Exit fullscreen mode

For more advanced theming, consider using a CSS-in-JS solution or tailwind classes.

6. Accessibility

Icons can be decorative or meaningful. For meaningful icons, always provide an aria-label and set role="img". For decorative icons, omit the label and use role="presentation" and aria-hidden="true".

In the Icon component above, we set the role dynamically based on the presence of aria-label.

7. Alternatives and Tools

If you prefer not to manage your own SVG library, there are other approaches:

  • Icon libraries: Packages like react-icons or @mui/icons-material offer thousands of ready-made icons as React components.
  • AI-powered icon generators: Tools like IcoGenie, Iconify, and Tabler Icons let you generate or download SVG icons tailored to your needs.
  • CDN-based SVG: Services like Heroicons let you copy-paste SVG code or fetch icons via CDN.

Each approach has trade-offs in terms of flexibility, bundle size, and customization.

Key Takeaways

A type-safe icon system in Next.js with TypeScript offers strong guarantees: you’ll never mistype an icon name, you’ll enjoy auto-completion, and you’ll have a single place to manage all your icons. By converting SVGs to React components (using SVGR), maintaining an icon map, and automating as much as possible, your codebase stays clean and maintainable as your icon set scales.

This approach ensures your Next.js icons are as robust, flexible, and developer-friendly as any other part of your modern React codebase.

Top comments (0)