A practical guide for React developers on integrating and customizing Shadcn icons in any existing project.
If you have been building React applications for a while, you know how much icons matter. They communicate actions, guide users, and give your UI a polished, intentional feel. But with so many icon libraries out there – Font Awesome, Heroicons, Phosphor, React Icons – the question becomes: which one plays nicely with your existing stack, especially if you’re using shadcn/ui?
This guide answers exactly that. We’ll explore how shadcn/ui handles icons and how to bring them into any existing project – the right way, with proper TypeScript types, accessibility in mind, and zero unnecessary bloat.
Table of Contents
What are Shadcn Icons?
“Shadcn icons” isn’t a standalone icon package you install from npm. Instead, shadcn/ui ships with a flexible icon system that lets you choose your preferred Shadcn icon library with Lucide React as the default.
Supported Shadcn Icon Libraries
| Icon Library | Packages | Style |
|---|---|---|
| Lucide (default) | lucide-react | Clean, minimal, outlined |
| Tabler Icons | @tabler/icons-react | Outlined, pixel-perfect |
| HugeIcons | @hugeicons/react, @hugeicons/core-free-icons | Bold, modern, rounded |
| Phosphor Icons | @phosphor-icons/react | Multi-weight, versatile |
| Remix Icon | @remixicon/react | Dual-tone, system-oriented |
You can set your Shadcn icon library during project creation or by updating the iconLibrary field in your components.json. Once set, shadcn/ui uses that library consistently across all generated components.
Lucide React remains the default and most widely used choice. When you browse shadcn/ui component source code, you’ll typically see:
import { Search, AlertCircle, Loader2 } from "lucide-react";
So when people say “shadcn icons,” they generally mean Lucide React icons within the shadcn/ui ecosystem, but this guide covers how to work with any of the supported libraries.
Each Shadcn icon library installs on its own via npm/pnpm/yarn – you don’t need the full shadcn/ui setup to use them.
How shadcn Icons Work Under the Hood
Unlike traditional icon libraries that bundle fonts or sprite sheets, shadcn icons are plain React components that render inline SVGs. No magic, just React components all the way down.
The components.json Config
When you set up a shadcn/ui project, your components.json file includes an iconLibrary field:
{
"$schema": "<https://ui.shadcn.com/schema.json>",
"style": "new-york",
"iconLibrary": "lucide",
...
}
This tells the shadcn CLI which icon package to use when generating components. When you run npx shadcn@latest add button, The CLI reads this config and wires in the correct icon imports automatically.
What Happens Inside a Component
Take the shadcn Alert component as an example. When iconLibrary is set to lucide, The generated code looks like:
import { AlertCircle } from "lucide-react";
export function AlertDemo() {
return (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Something went wrong.</AlertDescription>
</Alert>
);
}
If you had chosen tabler Instead, the same component would generate:
import { IconAlertCircle } from "@tabler/icons-react";
export function AlertDemo() {
return (
<Alert>
<IconAlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Something went wrong.</AlertDescription>
</Alert>
);
}
The component logic stays identical, only the icon import changes.
Why Inline SVGs?
Every icon renders as an inline <svg> in the DOM, which gives you:
- Full CSS control – You style them with
classNameTailwind utilities, or CSS-in-JS, are just like any other element. - No network requests – No font files or sprite sheets to download. Icons are part of your JS bundle.
- Tree-shakeable – You import only the icons you use. Unused icons are stripped out during build.
- SSR-safe – Inline SVGs render on the server with no hydration issues.
Here’s what a Lucide icon actually renders in the DOM:
<svg
xmlns="<http://www.w3.org/2000/svg>"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-circle-alert"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" x2="12" y1="8" y2="12"></line>
<line x1="12" x2="12.01" y1="16" y2="16"></line>
</svg>
Notice stroke="currentColor" – This is why icons automatically inherit the text color of their parent. Set text-red-500 on a parent <div>, and the icon turns red. No extra props needed.
React components → inline SVGs → styled with Tailwind. No fonts, no sprites, no runtime overhead.
Installing and Using Shadcn Icons
Setup takes under a minute regardless of where you’re adding it.
Installation
Install the icon library of your choice. We’ll use Lucide (the default) as the primary example, but the pattern is the same for all supported libraries:
# Lucide (default)
pnpm add lucide-react
# Or pick one of the others:
pnpm add @tabler/icons-react
pnpm add @hugeicons/react @hugeicons/core-free-icons
pnpm add @phosphor-icons/react
pnpm add @remixicon/react
Already using shadcn/ui? The icon library is installed automatically based on your
components.jsonconfig. You can skip this step.
Importing Icons
Each icon is a named export. Import only what you need:
import { Home, Settings, Bell } from "lucide-react";
The equivalent imports for other libraries:
// Tabler Icons
import { IconHome, IconSettings, IconBell } from "@tabler/icons-react";
// HugeIcons
import { HugeiconsIcon } from "@hugeicons/react";
import { Home07Icon, Setting07Icon, Notification01Icon } from "@hugeicons/core-free-icons";
// Phosphor Icons
import { HouseIcon, GearIcon, BellIcon } from "@phosphor-icons/react";
// Remix Icons
import { RiHome4Line, RiSettings2Line, RiNotification2Line } from "@remixicon/react";
Rendering Your First Icon
Lucide (and most libraries):
import { Home, Settings, Bell } from "lucide-react";
export function Navbar() {
return (
<nav className="flex items-center gap-4">
<Home className="size-5" />
<Settings className="size-5" />
<Bell className="size-5" />
</nav>
);
}
No extra config, no provider wrappers, no CSS imports.
Hugeicons works differently. Instead of using the icon component directly, you pass it as a prop to a HugeiconsIcon wrapper:
import { HugeiconsIcon } from "hugeicons-react";
import { Home07Icon, Setting07Icon, Notification01Icon } from "hugeicons-react";
export function Navbar() {
return (
<nav className="flex items-center gap-4">
<HugeiconsIcon icon={Home07Icon} strokeWidth={2} className="size-5" />
<HugeiconsIcon icon={Setting07Icon} strokeWidth={2} className="size-5" />
<HugeiconsIcon
icon={Notification01Icon}
strokeWidth={2}
className="size-5"
/>
</nav>
);
}
Top Shadcn Icons Library
| Library | Directory | Icons |
|---|---|---|
| Lucide | lucide.dev/icons | 1,500+ |
| Tabler | tabler.io/icons | 5,700+ |
| Hugeicons | hugeicons.com | 4,000+ |
| Phosphor | phosphoricons.com | 1,200+ (6 weights each) |
| Remix | remixicon.com | 2,800+ |
Search by keyword, copy the component name, and import it directly.
Special Mention: Shadcn Studio
If you are working on a shadcn project, you can also use the best Shadcn UI Library: Shadcn Studio

This isn’t a traditional component library or a replacement for Shadcn. Instead, it’s a unique collection that offers customizable variants of components, blocks, and templates. Preview, customize, and copy-paste them into your apps with ease.
Building on the solid foundation of the Shadcn components & blocks, we’ve enhanced it with custom-designed components & blocks to give you a head start. This allows you to craft, customize, and ship your projects faster and more efficiently.
Features:
- Open-source: Dive into a growing, community-driven collection of copy-and-paste 1000+ shadcn components and animated variants, Shadcn blocks, and templates.
- Component & Blocks variants: Access a diverse collection of customizable 700+ shadcn blocks and component variants to quickly build and style your UI with ease.
- Animated variants with Motion: Add smooth, modern animations to your components, enhancing user experiences with minimal effort.
- Landing pages & Dashboards: Explore 20+ premium & free Shadcn templates for dashboards, landing pages & more. Fully customizable & easy to use.
- shadcn/ui for Figma: Speed up your workflow with Shadcn Figma Design System, which consists of components, blocks & templates – a full design library inspired by shadcn/ui.
- Powerful theme generator: Customize your UI instantly with Shadcn Theme Generator. Preview changes in real time and create consistent, on-brand designs faster.
- shadcn/studio MCP: Integrate Shadcn MCP Server directly into your favorite IDE and craft stunning shadcn/ui Components, Blocks, and Pages inspired by shadcn/studio.
- Shadcn Figma To Code Plugin: Convert your Figma designs into production-ready code instantly with the Shadcn Figma Plugin.
Customizing Icons
Since shadcn icons are inline SVGs, you style them with Tailwind – the same way you’d style any element. Size, color, stroke thickness, fill, transforms – it’s all just utility classes on the className prop.
The key thing: icons use stroke="currentColor" by default, so they automatically inherit the text color of their parent. That one detail eliminates most manual color overrides.
Size – use size- utility*
<Home className="size-4" /> {/* 16px */}
<Home className="size-6" /> {/* 24px - most common */}
<Home className="size-8" /> {/* 32px */}
Color – icons inherit text color via currentColor
<Home className="size-5 text-gray-500" />
<Home className="size-5 text-primary" /> {/* shadcn theme token */}
<Home className="size-5 text-muted-foreground" /> {/* shadcn theme token */}
Stroke width
<Home className="size-5 stroke-[1.5]" /> {/* thinner */}
<Home className="size-5 stroke-[2.5]" /> {/* thicker */}
Fill – useful for toggle states like favorites
<Heart className="size-5 fill-red-500 stroke-red-500" />
Transforms and animations
<ChevronDown className="size-4 rotate-180" /> {/* flip direction */}
<Loader2 className="size-5 animate-spin" /> {/* loading spinner */}
- Parent color inheritance – Wrapping an icon in
<button className="text-blue-600 hover:text-blue-800">makes it follow the hover color automatically. No extra props. - Dark mode – Use shadcn theme tokens like
text-foregroundortext-muted-foregroundand icons adapt automatically. - Transitions –
transition-colors,transition-transform,hover:scale-110all work directly on the icon.
Using Icons in shadcn/ui Components
shadcn/ui components don’t need special wrappers or icon slots – drop the icon in as a child and the component handles spacing. The consistent convention: className="size-4" on every icon.
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Mail, Plus, Loader2, Search, CircleAlert } from "lucide-react";
// Button with icon
<Button>
<Mail className="size-4" />
Send Email
</Button>
// Icon-only button
<Button variant="outline" size="icon">
<Plus className="size-4" />
</Button>
// Loading state
<Button disabled>
<Loader2 className="size-4 animate-spin" />
Please wait
</Button>
// Search input with icon
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input className="pl-9" placeholder="Search..." />
</div>
// Alert with icon
<Alert variant="destructive">
<CircleAlert className="size-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Something went wrong.</AlertDescription>
</Alert>
For nav or menu patterns, pass icons as components in your config – easy to read, easy to change later:
const navItems = [
{ icon: Home, label: "Dashboard", href: "/" },
{ icon: Users, label: "Users", href: "/users" },
{ icon: Settings, label: "Settings", href: "/settings" }
]
navItems.map((item) => (
<a
key={item.href}
href={item.href}
className="flex items-center gap-3 px-3 py-2 text-sm text-muted-foreground hover:text-foreground"
>
<item.icon className="size-4" />
{item.label}
</a>
))
Accessibility
Icons look great, but they can create problems for screen reader users if you’re not careful. The key question is: does this icon convey meaning, or is it purely decorative?
Decorative Icons
Most icons in a UI sit next to text that already describes the action – a trash can icon next to the word “Delete”, for example. These icons are decorative and should be hidden from screen readers:
// The text "Delete" already communicates the action
<Button>
<Trash2 className="size-4" aria-hidden="true" />
Delete
</Button>
Lucide sets aria-hidden="true" on the SVG automatically — unless you pass an accessibility prop like aria-label or aria-labelledby. So decorative icons are handled for you. The ones that need attention are icon-only interactive elements, covered below.
Meaningful Icons
When an icon is the only element communicating an action – like an icon-only button – you need to provide an accessible label:
// Icon-only button - needs an accessible name
<Button variant="outline" size="icon" aria-label="Delete item">
<Trash2 className="size-4" />
</Button>
// Icon-only link
<a href="/settings" aria-label="Settings">
<Settings className="size-4" />
</a>
Place the aria-label on the interactive parent (button, link), not on the icon itself. This ensures screen readers announce “Delete item, button” rather than just “button”.
Quick Rule of Thumb
- Icon with visible text → decorative → add
aria-hidden="true"to the icon - Icon without visible text → meaningful → add
aria-labelto the parent interactive element
Performance
The icon approach here is fast by default – tree shaking handles most of it automatically. But two mistakes are common enough to call out.
Tree Shaking
All five supported libraries are tree-shakable. When you write:
import { Home, Settings } from "lucide-react";
Only Home and Settings end up in your bundle – the other 1,500+ icons are stripped out during build. This works automatically with any modern bundler (Webpack, Vite, Turbopack).
Avoid importing the entire library:
// ❌ Bad - pulls in every icon
import * as Icons from "lucide-react";
// ✅ Good - only imports what you use
import { Home, Settings } from "lucide-react";
Bundle Size
Each icon is roughly 1–2 KB gzipped. Fifty icons is ~50–100 KB total – not worth worrying about.
If you’re above 100 icons, run @next/bundle-analyzer – You’ll usually find a handful that are imported but never actually rendered.
Avoid Creating Icons at Render Time
The other mistake: building an icon map inside the component body.
// ❌ Bad - creates a new component reference every render
function MyComponent({ iconName }: { iconName: string }) {
const icons = { Home, Settings, Users };
const Icon = icons[iconName]; // new reference each render
return <Icon className="size-4" />;
}
// ✅ Good - stable reference via useMemo or defined outside render
const iconMap = { Home, Settings, Users } as const;
function MyComponent({ iconName }: { iconName: keyof typeof iconMap }) {
const Icon = iconMap[iconName];
return <Icon className="size-4" />;
}
The map defined outside the component keeps references stable across renders.
Conclusion
No icon fonts, no sprite sheets, no extra config. Pick a library, install it, import what you need – the workflow is the same regardless of which one you choose:
- Install the icon package
- Import only the icons you need
- Style them with Tailwind classes like any other element
These libraries work in any React project – you don’t need shadcn/ui installed, and they sit alongside whatever else you’re already using.
If you’re starting a new shadcn/ui project, set your preferred iconLibrary in components.json and the CLI handles the rest. If you’re adding icons to an existing project, just install the package and start importing.
Helpful Links
- Lucide Icons – Browse and search the default icon library
- Tabler Icons – 5,700+ pixel-perfect icons
- Phosphor Icons – Multi-weight icon family
- Hugeicons – Modern, rounded icon set
- Remix Icon – System-oriented dual-tone icons
- shadcn/ui Docs – Official documentation
- shadcn/ui Create – Pick your style, colors, and icon library before starting a project