How to Add Custom Icon Libraries Beyond Lucide to shadcn/ui Components

Extend the iconLibraries map in packages/shadcn/src/icons/libraries.ts, install the npm package, set the iconLibrary field in your components.json, and use library-specific props on the IconPlaceholder component to integrate any React icon set into shadcn/ui.

The shadcn/ui framework ships with a flexible icon system that defaults to Lucide but supports any React icon library exporting individual components. By registering new entries in the source registry and leveraging the IconPlaceholder abstraction, you can add custom icon libraries to shadcn components without modifying component internals. This approach uses type-safe configuration and build-time transformation to swap placeholder components for concrete icon imports.

How the Icon Library System Works

shadcn/ui centralizes icon management through a typed constant called iconLibraries located in packages/shadcn/src/icons/libraries.ts. This map defines metadata for each supported library, including package names, import patterns, and usage templates.

When you build or initialize components, the transform-icons.ts utility reads your components.json configuration, looks up the active library in iconLibraries, and replaces <IconPlaceholder /> instances with the correct import statements. Because iconLibraries uses as const assertion, TypeScript automatically infers valid library names, ensuring the Zod schema in apps/v4/registry/config.ts and runtime validation remain synchronized without manual type definition updates.

Step-by-Step Implementation

Follow these steps to register and use a custom icon library like Heroicons or Tabler Icons.

1. Register the Library in Source Code

Add a new entry to the iconLibraries object in packages/shadcn/src/icons/libraries.ts. Define the name, title, packages array, import template, usage pattern, and export path.

// packages/shadcn/src/icons/libraries.ts
export const iconLibraries = {
  lucide: { /* existing */ },
  // New library entry
  heroicons: {
    name: "heroicons",
    title: "Heroicons",
    packages: ["@heroicons/react"],
    import: "import { ICON } from '@heroicons/react/24/outline'",
    usage: "<ICON />",
    export: "@heroicons/react",
  },
} as const;

The as const assertion ensures TypeScript generates the IconLibraryName union type automatically, extending validation schemas immediately.

2. Install the NPM Package

Install the icon library in your consuming project or workspace root.

npm i @heroicons/react

# or pnpm add @heroicons/react

3. Configure Your Component

Update your components.json to specify the new library using the iconLibrary field. If omitted, get-config.ts defaults to "lucide".

{
  "components": [
    {
      "name": "button",
      "iconLibrary": "heroicons",
      "files": ["./src/components/ui/button.tsx"]
    }
  ]
}

4. Use the Icon Placeholder in JSX

Reference icons using the library-specific prop on the IconPlaceholder component. The prop name matches the library key defined in step 1.

import { IconPlaceholder } from "@/components/ui/icon-placeholder";

export function AddButton() {
  return (
    <button className="flex items-center gap-2">
      <IconPlaceholder heroicons="AcademicCapIcon" className="size-4" />
      Add Item
    </button>
  );
}

During the build process, transform-icons.ts replaces this placeholder with:

import { AcademicCapIcon as Icon } from "@heroicons/react/24/outline";
// ...
<Icon className="size-4" />

Technical Architecture Details

Build-Time Transformation

The transformation logic resides in packages/shadcn/src/utils/transformers/transform-icons.ts. This utility parses JSX AST, validates the iconLibrary value against the iconLibraries map, and generates concrete import statements. It also strips incompatible library-specific props to prevent runtime errors.

Configuration Loading

The get-config.ts file in packages/shadcn/src/utils/get-config.ts ensures every component configuration contains an iconLibrary field, injecting "lucide" as the default when unspecified. It validates the final value against the inferred IconLibraryName type.

Registry Validation

The apps/v4/registry/config.ts file defines a Zod schema that includes iconLibrary: z.enum([...]). Because the enum derives from iconLibraries, adding a new library in the source code automatically extends allowed values for registry validation.

Legacy Support (Optional)

For backward compatibility with older component definitions, map legacy library names in packages/shadcn/src/utils/legacy-icon-libraries.ts. This step is only required if you need to support existing components using deprecated library identifiers.

Complete Example: Adding Heroicons

Here is the end-to-end implementation for integrating Heroicons into a shadcn/ui project.

First, extend the registry:

// packages/shadcn/src/icons/libraries.ts
export const iconLibraries = {
  lucide: {
    name: "lucide",
    title: "Lucide",
    packages: ["lucide-react"],
    import: "import { ICON } from 'lucide-react'",
    usage: "<ICON />",
    export: "lucide-react",
  },
  heroicons: {
    name: "heroicons",
    title: "Heroicons",
    packages: ["@heroicons/react"],
    import: "import { ICON } from '@heroicons/react/24/outline'",
    usage: "<ICON />",
    export: "@heroicons/react",
  },
} as const;

Install the dependency:

pnpm add @heroicons/react

Configure the component:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "components": [
    {
      "name": "alert",
      "iconLibrary": "heroicons",
      "files": ["app/components/ui/alert.tsx"]
    }
  ]
}

Use in your component:

import { IconPlaceholder } from "@/components/ui/icon-placeholder";

export function Alert() {
  return (
    <div className="rounded border p-4">
      <IconPlaceholder heroicons="ExclamationTriangleIcon" className="size-4 text-amber-500" />
      <span>Warning message</span>
    </div>
  );
}

Run your build command to execute the transformation.

Summary

  • Register new libraries by adding entries to iconLibraries in packages/shadcn/src/icons/libraries.ts using as const for automatic type inference.
  • Install the npm package in your project workspace.
  • Configure the iconLibrary field in components.json to select your custom library.
  • Implement icons using IconPlaceholder with library-specific props (e.g., heroicons="IconName").
  • Transform placeholders into concrete imports automatically via transform-icons.ts during the build process.

Frequently Asked Questions

Do I need to manually update TypeScript types when adding a new icon library?

No. Because iconLibraries uses as const assertion in packages/shadcn/src/icons/libraries.ts, TypeScript automatically infers the IconLibraryName union type. This propagates to the Zod schema in apps/v4/registry/config.ts and runtime validation without manual type definition changes.

Can I use multiple icon libraries in the same project?

Yes, but on a per-component basis. Each component in components.json specifies its own iconLibrary. However, within a single component file, you should stick to one library per IconPlaceholder usage, as the transformer replaces all placeholders based on that component's configured library.

What happens if I don't specify an iconLibrary in components.json?

The get-config.ts utility automatically defaults the value to "lucide". Your component will use Lucide React icons unless you explicitly override the iconLibrary field in the configuration.

Why does shadcn/ui use IconPlaceholder instead of direct imports?

The IconPlaceholder abstraction decouples component source code from specific icon libraries. This allows the transform-icons.ts build-time transformer to swap in the correct imports based on your components.json configuration, enabling the same component code to work with Lucide, Heroicons, or any future library without source modifications.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →