React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

lucideiconsreactsvgtree-shaking

Lucide React Icons

Recipe

Install Lucide React, import individual icons by name, and customize them with props or Tailwind classes. Each icon is a separate module, so your bundle only includes the icons you actually use.

npm install lucide-react
// app/components/icon-demo.tsx
"use client";
 
import { Search, Menu, X, Settings, Bell } from "lucide-react";
 
export function IconDemo() {
  return (
    <div className="flex items-center gap-4">
      <Search size={24} color="currentColor" strokeWidth={2} />
      <Menu size={24} className="text-gray-600" />
      <X size={20} className="text-red-500" />
      <Settings size={24} strokeWidth={1.5} />
      <Bell size={24} className="text-blue-600" />
    </div>
  );
}

Working Example

An icon button toolbar with hover states and accessibility labels:

// app/components/icon-toolbar.tsx
"use client";
 
import { Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, Link, Image } from "lucide-react";
import { useState } from "react";
 
interface ToolbarButton {
  icon: React.ElementType;
  label: string;
  action: string;
}
 
const tools: ToolbarButton[] = [
  { icon: Bold, label: "Bold", action: "bold" },
  { icon: Italic, label: "Italic", action: "italic" },
  { icon: Underline, label: "Underline", action: "underline" },
  { icon: AlignLeft, label: "Align Left", action: "align-left" },
  { icon: AlignCenter, label: "Align Center", action: "align-center" },
  { icon: AlignRight, label: "Align Right", action: "align-right" },
  { icon: Link, label: "Insert Link", action: "link" },
  { icon: Image, label: "Insert Image", action: "image" },
];
 
export function IconToolbar() {
  const [active, setActive] = useState<string | null>(null);
 
  return (
    <div className="flex items-center gap-1 rounded-lg border border-gray-200 bg-white p-2">
      {tools.map(({ icon: Icon, label, action }) => (
        <button
          key={action}
          onClick={() => setActive(active === action ? null : action)}
          aria-label={label}
          aria-pressed={active === action}
          className={`rounded p-2 transition-colors hover:bg-gray-100 ${
            active === action ? "bg-blue-100 text-blue-600" : "text-gray-600"
          }`}
        >
          <Icon size={18} strokeWidth={active === action ? 2.5 : 2} />
        </button>
      ))}
    </div>
  );
}

Deep Dive

How It Works

  • Lucide is a fork of Feather Icons with an expanded icon set (over 1,400 icons) and active maintenance.
  • Each icon is exported as a named export from lucide-react, but also available as a standalone module at lucide-react/dist/esm/icons/{icon-name}.
  • Icons render as inline SVG elements with sensible defaults: 24x24 size, 2px stroke width, currentColor stroke.
  • Tree-shaking works automatically with modern bundlers (webpack 5, Vite, Turbopack) because each icon is a separate ES module entry.
  • All icons accept standard SVG attributes and React HTML attributes, including className, style, onClick, and ARIA attributes.

Variations

Dynamic icon rendering from a string name:

import { icons } from "lucide-react";
import type { LucideIcon } from "lucide-react";
 
interface DynamicIconProps {
  name: string;
  size?: number;
  className?: string;
}
 
export function DynamicIcon({ name, size = 24, className }: DynamicIconProps) {
  const IconComponent = icons[name as keyof typeof icons] as LucideIcon | undefined;
 
  if (!IconComponent) {
    return null;
  }
 
  return <IconComponent size={size} className={className} />;
}
 
// Usage: <DynamicIcon name="ArrowRight" size={20} className="text-blue-500" />

Note: Importing the entire icons object defeats tree-shaking. Only use dynamic rendering when you genuinely need it (e.g., CMS-driven icon selection).

Custom icon wrapper component:

import type { LucideIcon } from "lucide-react";
 
interface IconButtonProps {
  icon: LucideIcon;
  label: string;
  onClick: () => void;
  variant?: "default" | "danger" | "success";
}
 
const variantStyles = {
  default: "text-gray-600 hover:bg-gray-100",
  danger: "text-red-600 hover:bg-red-50",
  success: "text-green-600 hover:bg-green-50",
};
 
export function IconButton({ icon: Icon, label, onClick, variant = "default" }: IconButtonProps) {
  return (
    <button
      onClick={onClick}
      aria-label={label}
      className={`rounded-lg p-2 transition-colors ${variantStyles[variant]}`}
    >
      <Icon size={20} />
    </button>
  );
}

TypeScript Notes

  • Import LucideIcon type to type icon components passed as props.
  • All icon components accept LucideProps, which extends SVGProps<SVGSVGElement> with size, color, strokeWidth, and absoluteStrokeWidth.
  • The icons object is typed as Record<string, LucideIcon>, enabling type-safe dynamic lookups.
import type { LucideIcon, LucideProps } from "lucide-react";
 
// Type an icon prop
interface Props {
  icon: LucideIcon;
  iconProps?: LucideProps;
}

Gotchas

  • Importing from lucide-react with barrel imports like import * as icons will bundle every icon (over 1,400 SVGs). Always use named imports for production builds.
  • The color prop sets the SVG stroke attribute, not fill. Lucide icons are stroke-based, so fill has no visible effect on most icons.
  • If you use className to set color via Tailwind (e.g., text-blue-500), do not also set the color prop, as the explicit prop overrides currentColor.
  • size sets both width and height simultaneously. For non-square sizing, use width and height props individually.
  • Dynamic imports using icons[name] provide no compile-time validation that the icon name exists. Consider building a whitelist for CMS-driven scenarios.

Alternatives

ApproachProsCons
Lucide ReactExcellent tree-shaking, large icon set, active maintenanceStroke-only style may not fit all designs
React IconsMultiple icon families in one packageLarger install size, inconsistent APIs across families
HeroiconsOfficial Tailwind Labs project, great Tailwind integrationSmaller icon set (around 300 icons)
Custom SVG componentsFull design control, zero dependenciesManual maintenance, no icon discovery

FAQs

How do you install and import a Lucide icon in a React component?
npm install lucide-react
 
import { Search } from "lucide-react";
 
export function MyComponent() {
  return <Search size={24} className="text-gray-600" />;
}
What are the default size, stroke width, and color for Lucide icons?
  • Default size is 24x24 pixels.
  • Default stroke width is 2px.
  • Default color is currentColor, which inherits from the parent CSS color property.
Why does Lucide React have good tree-shaking, and how can you accidentally break it?
  • Each icon is a separate ES module entry, so bundlers only include icons you import by name.
  • Importing the entire icons object (import { icons } from "lucide-react") bundles all 1,400+ icons and defeats tree-shaking.
How do you render a Lucide icon dynamically from a string name?
import { icons } from "lucide-react";
import type { LucideIcon } from "lucide-react";
 
const IconComponent = icons[name as keyof typeof icons] as LucideIcon | undefined;
if (IconComponent) return <IconComponent size={24} />;

Note: this imports all icons and disables tree-shaking.

What happens if you set both the color prop and a Tailwind text color class on a Lucide icon?
  • The explicit color prop overrides currentColor.
  • The Tailwind class (e.g., text-blue-500) sets currentColor, but the prop takes precedence.
  • Use one or the other, not both.
How do you create a reusable icon button wrapper with variant styles?
import type { LucideIcon } from "lucide-react";
 
interface IconButtonProps {
  icon: LucideIcon;
  label: string;
  onClick: () => void;
  variant?: "default" | "danger";
}
 
export function IconButton({ icon: Icon, label, onClick, variant = "default" }: IconButtonProps) {
  return (
    <button onClick={onClick} aria-label={label}>
      <Icon size={20} />
    </button>
  );
}
Gotcha: Why does setting fill have no visible effect on most Lucide icons?
  • Lucide icons are stroke-based, not fill-based.
  • The color prop controls the stroke attribute, not fill.
  • Setting fill="red" on a stroke icon will not change its appearance.
How do you make a Lucide icon non-square (different width and height)?
  • The size prop sets both width and height to the same value.
  • For non-square sizing, pass width and height props individually instead of size.
What TypeScript type should you use when passing a Lucide icon as a prop?
import type { LucideIcon, LucideProps } from "lucide-react";
 
interface Props {
  icon: LucideIcon;
  iconProps?: LucideProps;
}

LucideIcon types the component itself; LucideProps extends SVGProps<SVGSVGElement>.

How does the icon toolbar example handle active state toggling and accessibility?
  • It uses useState to track which tool is active.
  • Each button has aria-label for screen readers and aria-pressed to indicate toggle state.
  • Active icons get a thicker strokeWidth (2.5 vs 2) and a blue background.
Gotcha: What risk does dynamic icon lookup from CMS data introduce?
  • icons[name] provides no compile-time validation that the icon name exists.
  • A typo or missing icon silently returns undefined.
  • Build a whitelist of allowed icon names for CMS-driven scenarios.
What is the icons object typed as in lucide-react?
  • It is typed as Record<string, LucideIcon>.
  • This enables type-safe dynamic lookups but still requires a runtime existence check since any string key is accepted.