Lucide vs Heroicons vs Phosphor: React Icon Libraries in 2026
Lucide vs Heroicons vs Phosphor: React Icon Libraries in 2026
TL;DR
Lucide is the most actively maintained fork of Feather Icons — consistent design language, 1,500+ icons, excellent tree-shaking, and a clean React API. Heroicons is Tailwind CSS's official icon library — curated set of 292 polished icons in outline and solid variants, perfect if you're in the Tailwind ecosystem. Phosphor Icons has the most icons (7,700+) in the most weights (thin, light, regular, bold, fill, duotone) — unmatched variety, but at the cost of a larger surface area. For most Tailwind-first projects, Heroicons. For comprehensive icon needs, Phosphor. For the Feather-style aesthetic with active maintenance, Lucide.
Key Takeaways
- Phosphor React has 7,700+ icons in 6 weight variants — by far the largest collection
- Heroicons has the smallest bundle per icon — only 292 icons but each is highly optimized SVG
- Lucide React npm downloads: ~5M/week — the most popular standalone icon library for React
- All three are tree-shakeable — unused icons are excluded from your bundle
- Phosphor's 6 weights (thin, light, regular, bold, fill, duotone) enable visual hierarchy without multiple libraries
- Heroicons requires zero custom props for basic use — minimal API, works with Tailwind size classes
- Lucide allows stroke width customization — rare feature that significantly affects visual weight
Icon Libraries in 2026
With the rise of design systems and Figma-first workflows, icon libraries have become critical infrastructure. The key questions are:
- Does it match your design aesthetic?
- Does tree-shaking work correctly?
- How many icons are available?
- Can you customize stroke weight and size easily?
Lucide: The Feather Fork That Won
Lucide started as a fork of Feather Icons when Feather went unmaintained. It now has 1,500+ icons (vs Feather's 286), an active community, and consistent visual design.
Installation
npm install lucide-react
Basic Usage
import { Search, Bell, Settings, User, ChevronRight, Loader2 } from "lucide-react";
// Default size 24, stroke-width 2, color currentColor
function Navbar() {
return (
<nav className="flex items-center gap-4">
<Search size={20} />
<Bell size={20} strokeWidth={1.5} /> {/* Thinner stroke */}
<Settings size={20} className="text-gray-500" />
</nav>
);
}
Customization API
import { AlertCircle, CheckCircle, XCircle, Info } from "lucide-react";
// Lucide's unique feature: adjustable strokeWidth
function AlertBadge({ type }: { type: "error" | "success" | "warning" | "info" }) {
const iconMap = {
error: <XCircle size={16} strokeWidth={2.5} className="text-red-500" />,
success: <CheckCircle size={16} strokeWidth={2} className="text-green-500" />,
warning: <AlertCircle size={16} strokeWidth={1.5} className="text-yellow-500" />,
info: <Info size={16} strokeWidth={2} className="text-blue-500" />,
};
return iconMap[type];
}
// Custom default props — apply globally
import { createLucideIcon } from "lucide-react";
// All icons in your app use stroke-width 1.5 by default
const AppIcon = ({ Icon, ...props }: { Icon: React.ComponentType<any> }) => (
<Icon strokeWidth={1.5} {...props} />
);
Icon Creation (Custom Icons)
import { createLucideIcon } from "lucide-react";
// Create a custom icon with Lucide's API
const BrandLogo = createLucideIcon("BrandLogo", [
["circle", { cx: "12", cy: "12", r: "10" }],
["path", { d: "M8 12h8M12 8v8" }],
]);
// Use like any Lucide icon
<BrandLogo size={24} strokeWidth={2} className="text-blue-600" />
Dynamic Icons
// Load icons by name — useful for CMS-driven UIs
import { icons } from "lucide-react";
import type { LucideIcon } from "lucide-react";
interface IconProps {
name: keyof typeof icons;
size?: number;
className?: string;
}
export function DynamicIcon({ name, size = 24, className }: IconProps) {
const Icon: LucideIcon = icons[name];
if (!Icon) return null;
return <Icon size={size} className={className} />;
}
// Usage
<DynamicIcon name="Settings" size={20} className="text-gray-600" />
Tree-Shaking Verification
# Check bundle impact
npx bundlephobia lucide-react
# Full library: ~1.8 MB
# Per icon (tree-shaken): ~0.5 KB avg
# With Next.js, check in .next/static/chunks/
# Each used icon adds ~0.5 KB
Heroicons: Tailwind's Official Icons
Heroicons is designed and maintained by the Tailwind CSS team. It ships 292 icons in 3 variants: outline (24px), solid (24px), and mini (20px). The design is polished and integrates seamlessly with Tailwind size utilities.
Installation
npm install @heroicons/react
Basic Usage
// Three variants: outline, solid, mini
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { MagnifyingGlassIcon as MagnifyingGlassIconSolid } from "@heroicons/react/24/solid";
import { MagnifyingGlassIcon as MagnifyingGlassIconMini } from "@heroicons/react/20/solid";
function SearchBar() {
return (
<div className="relative">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input className="pl-10 pr-4 py-2 border rounded-lg" placeholder="Search..." />
</div>
);
}
Solid vs Outline
import { HeartIcon } from "@heroicons/react/24/outline";
import { HeartIcon as HeartIconSolid } from "@heroicons/react/24/solid";
function LikeButton({ liked, onToggle }: { liked: boolean; onToggle: () => void }) {
return (
<button
onClick={onToggle}
className="flex items-center gap-1 text-sm"
>
{liked ? (
<HeartIconSolid className="h-5 w-5 text-red-500" />
) : (
<HeartIcon className="h-5 w-5 text-gray-400 hover:text-red-400" />
)}
{liked ? "Liked" : "Like"}
</button>
);
}
With Tailwind Class Variants
import {
CheckCircleIcon,
ExclamationCircleIcon,
InformationCircleIcon,
XCircleIcon,
} from "@heroicons/react/24/solid";
type AlertType = "success" | "error" | "warning" | "info";
const alertConfig = {
success: { Icon: CheckCircleIcon, color: "text-green-500", bg: "bg-green-50" },
error: { Icon: XCircleIcon, color: "text-red-500", bg: "bg-red-50" },
warning: { Icon: ExclamationCircleIcon, color: "text-yellow-500", bg: "bg-yellow-50" },
info: { Icon: InformationCircleIcon, color: "text-blue-500", bg: "bg-blue-50" },
};
function Alert({ type, message }: { type: AlertType; message: string }) {
const { Icon, color, bg } = alertConfig[type];
return (
<div className={`flex items-center gap-3 p-4 rounded-lg ${bg}`}>
<Icon className={`h-5 w-5 shrink-0 ${color}`} />
<p className="text-sm">{message}</p>
</div>
);
}
Bundle Impact
# Heroicons is organized into separate import paths
# Each variant is a separate package chunk
# @heroicons/react/24/outline → ~150 KB (full set)
# Per icon: ~0.4 KB tree-shaken
# Tree-shaking works automatically with named imports
import { ArrowRightIcon } from "@heroicons/react/24/outline"; // Only this icon
Phosphor Icons: Maximum Variety
Phosphor has 7,700+ icons across 6 weight variants. If you need a specific icon for a niche use case, Phosphor almost certainly has it.
Installation
npm install @phosphor-icons/react
Six Weights
import {
Heart, // Regular (default)
Heart as HeartThin, // Override weight prop
Heart as HeartBold, // Override weight prop
} from "@phosphor-icons/react";
function WeightDemo() {
return (
<div className="flex gap-4 items-center">
<Heart size={32} weight="thin" /> {/* Thinnest */}
<Heart size={32} weight="light" />
<Heart size={32} weight="regular" /> {/* Default */}
<Heart size={32} weight="bold" />
<Heart size={32} weight="fill" /> {/* Filled */}
<Heart size={32} weight="duotone" /> {/* Two-tone */}
</div>
);
}
Duotone Icons (Unique Feature)
import { CloudArrowUp, Database, Shield } from "@phosphor-icons/react";
// Duotone: body is 30% opacity, accent at full opacity
function FeatureCard({ title, description }: { title: string; description: string }) {
return (
<div className="p-6 bg-white rounded-xl border">
<CloudArrowUp size={48} weight="duotone" className="text-blue-600 mb-4" />
<h3 className="font-semibold">{title}</h3>
<p className="text-gray-500 text-sm mt-1">{description}</p>
</div>
);
}
Global Context (Set Defaults)
import { IconContext } from "@phosphor-icons/react";
// Set default props for all icons in a subtree
function App() {
return (
<IconContext.Provider value={{ size: 20, weight: "bold", mirrored: false }}>
<main>
{/* All icons here use size=20, weight=bold */}
<Navigation />
<Content />
</main>
</IconContext.Provider>
);
}
Bundle Size Considerations
# Phosphor is large — but tree-shaking handles it
npx bundlephobia @phosphor-icons/react
# Full library: ~14 MB
# Per icon (tree-shaken): ~1 KB avg (slightly larger than Lucide due to variants)
# With duotone icons, each icon has two SVG paths
# Recommendation: use consistent weight across your app to minimize variants loaded
Feature Comparison
| Feature | Lucide | Heroicons | Phosphor |
|---|---|---|---|
| Icons count | 1,500+ | 292 | 7,700+ |
| Style variants | Outline only | Outline + Solid + Mini | 6 weights |
| Duotone icons | ❌ | ❌ | ✅ |
| Stroke width control | ✅ (unique) | ❌ | ❌ |
| Tree-shakeable | ✅ | ✅ | ✅ |
| TypeScript | ✅ | ✅ | ✅ |
| Per-icon size (tree-shaken) | ~0.5 KB | ~0.4 KB | ~1 KB |
| Dynamic loading | ✅ | ❌ | ✅ |
| Context/default props | ❌ | ❌ | ✅ |
| npm downloads/week | ~5M | ~2M | ~700k |
| Tailwind ecosystem fit | ✅ | ✅ Native | ✅ |
| Design aesthetic | Minimal/clean | Polished/Tailwind | Versatile |
| Custom icon API | ✅ | ❌ | ❌ |
Bundle Size Reality Check
Scenario: Use 50 icons in a production app
Lucide: 50 × 0.5 KB = 25 KB (gzipped: ~8 KB)
Heroicons: 50 × 0.4 KB = 20 KB (gzipped: ~7 KB)
Phosphor: 50 × 1 KB = 50 KB (gzipped: ~15 KB)
All three are negligible in the context of a real app.
The bundle size difference doesn't matter in practice.
The decision should be based on aesthetics and feature needs.
When to Use Each
Choose Lucide if:
- You want the Feather Icons aesthetic (clean, minimal, stroke-based)
- You need
strokeWidthcustomization for visual hierarchy - You need a custom icon that matches the Lucide style via
createLucideIcon - Your app has a variety of icon sizes and you need consistent weight control
Choose Heroicons if:
- You're using Tailwind CSS and want icons from the same design team
- Your icon needs are modest — you need 50–100 common UI icons
- You want solid/outline variants for interactive states (like/unlike, bookmark)
- Zero dependencies and minimal API surface matters
Choose Phosphor if:
- You need niche or domain-specific icons not found in smaller libraries
- Duotone icons fit your design system (marketing pages, feature callouts)
- You need multiple weights (thin for decorative, bold for interactive) across your app
- You're building a design system that needs icon weight as a design token
Methodology
Data sourced from npm download statistics (npmjs.com, January 2026), GitHub repositories, official documentation, and bundle size measurements from Bundlephobia. Icon counts verified from official GitHub repositories as of February 2026. npm weekly downloads: Lucide React (5M), Heroicons (2M), Phosphor React (700k).
Related: React Component Libraries 2026 for full UI frameworks, or Lottie vs Rive vs CSS Animations for animated icons and graphics.