TL;DR: Lucide React is 1500+ pixel-perfect SVG icons as tree-shakeable React components. Import only what you use - each icon adds ~1kB. It's the default icon set in shadcn/ui. Consistent stroke style across the whole set. Official docs: lucide.dev.
What is Lucide React?
Lucide is a community fork of Feather Icons with roughly triple the icon count, active maintenance, and consistent design rules. Each icon is a React component that renders an inline SVG - there's no icon font, no sprite sheet, and no separate asset pipeline.
Icons are tree-shakeable: if you import only CircleCheck and AlertTriangle, only those two SVGs end up in your bundle. That matters more than it sounds. Icon fonts force you to download the entire glyph set even if you use a dozen icons, and they bring their own rendering quirks like blurry edges and baseline alignment headaches. Inline SVG sidesteps all of that. Each icon scales crisply, inherits text color through currentColor, and styles with plain CSS.
Because every icon is drawn on the same 24-pixel grid with the same 2-pixel stroke, the whole set looks like it came from one hand. That visual consistency is the quiet reason designers like it. Mixing icons from three different packs always shows, and Lucide saves you from that by giving you one coherent family that covers nearly everything a typical app needs.
Choosing Lucide for your project
Use Lucide when you want a consistent, large icon set with zero configuration. It's the default icon library in shadcn/ui and is widely used across the React ecosystem, so if you're already in that world the icons will feel at home. The API is about as small as it gets: import a component, set a size, set a color, done.
How does it stack up against Heroicons and Phosphor? All three are solid. Lucide has the broadest selection and the most active community. Heroicons is more opinionated and leans on the Tailwind team's aesthetic, with outline and solid variants only. Phosphor goes the other direction with six weight variants per icon, from thin to bold to duotone, which is great when you want stylistic range but adds decisions you may not want to make. If you just need a big, tasteful, no-fuss set, Lucide is the path of least resistance.
One practical note: icon names occasionally change between major versions as the project tidies its naming. Pin your version and read the changelog before bumping, so a renamed icon doesn't quietly disappear from your build.
Key Features
- 1500+ icons - covers UI, navigation, media, devices, file types, and more
- SVG props passthrough -
size,color,strokeWidth, and all standard SVG attributes - Dynamic imports - use
LucidePropstype +React.lazyfor lazy-loaded icon pickers lucide-react/dynamic- import icons by name string at runtime
Installation
npm install lucide-react
Basic Usage
import { Search, CircleCheck, AlertTriangle, Loader2 } from 'lucide-react'
function StatusIcon({ status }: { status: 'success' | 'warning' | 'loading' }) {
if (status === 'success') return <CircleCheck className="text-green-500" size={20} />
if (status === 'warning') return <AlertTriangle className="text-yellow-500" size={20} />
return <Loader2 className="animate-spin text-gray-400" size={20} />
}
Using Icons as Button Labels
Always pair icon-only buttons with accessible labels:
import { Trash2 } from 'lucide-react'
function DeleteButton({ onDelete }: { onDelete: () => void }) {
return (
<button
onClick={onDelete}
aria-label="Delete item"
className="p-2 rounded hover:bg-red-50 text-red-500"
>
<Trash2 size={16} aria-hidden="true" />
</button>
)
}
Dynamic Icon Lookup
import { icons, type LucideIcon } from 'lucide-react'
function Icon({ name, ...props }: { name: string } & React.SVGProps<SVGSVGElement>) {
const LucideIcon = icons[name as keyof typeof icons] as LucideIcon | undefined
if (!LucideIcon) return null
return <LucideIcon {...props} />
}
// Usage:
<Icon name="Rocket" size={24} />
TypeScript Tips
Use the LucideIcon type when accepting icons as props:
import { type LucideIcon } from 'lucide-react'
interface NavItem {
label: string
href: string
icon: LucideIcon
}
function NavLink({ item }: { item: NavItem }) {
return (
<a href={item.href} className="flex items-center gap-2">
<item.icon size={18} aria-hidden="true" />
{item.label}
</a>
)
}
Bundle size and dynamic loading
For most apps, importing named icons is exactly right and the tree-shaking handles the rest. The trouble starts when you build something like an icon picker that needs access to all 1500-plus icons at once. Importing the entire set defeats tree-shaking and balloons your bundle. For that case, use the lucide-react/dynamic entry point, which lets you load an icon by its name string at runtime and code-splits the rest away. Your picker stays light because it only pulls in the icon a user actually selects.
There's a build-time wrinkle worth knowing. In some bundler setups, importing many icons from the barrel file can slow down dev startup, because the tool processes the whole module map. If you notice sluggish cold starts, importing from the specific icon path can help. Most modern setups handle the barrel import fine, so measure before you optimize.
Common Gotchas
sizevswidth/height- use thesizeprop (Lucide shorthand) rather thanwidthandheightseparately, so the icon stays square.strokeWidth- default is2. Reducing to1.5gives a lighter feel; increasing to2.5reads better at small sizes.- Accessibility - add
aria-hidden="true"to decorative icons andaria-labelto the parent button; don't putrole="img"on decorative SVGs. colorandcurrentColor- by default icons usecurrentColor, so set color on the parent with text utilities rather than passing a hardcodedcolorprop you'll forget to theme later.