Skip to main content

Lucide vs Heroicons vs Phosphor: React Icon Libraries in 2026

·PkgPulse Team

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:

  1. Does it match your design aesthetic?
  2. Does tree-shaking work correctly?
  3. How many icons are available?
  4. 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

FeatureLucideHeroiconsPhosphor
Icons count1,500+2927,700+
Style variantsOutline onlyOutline + Solid + Mini6 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 aestheticMinimal/cleanPolished/TailwindVersatile
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 strokeWidth customization 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.