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 atlucide-react/dist/esm/icons/{icon-name}. - Icons render as inline SVG elements with sensible defaults: 24x24 size, 2px stroke width,
currentColorstroke. - 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
LucideIcontype to type icon components passed as props. - All icon components accept
LucideProps, which extendsSVGProps<SVGSVGElement>withsize,color,strokeWidth, andabsoluteStrokeWidth. - The
iconsobject is typed asRecord<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-reactwith barrel imports likeimport * as iconswill bundle every icon (over 1,400 SVGs). Always use named imports for production builds. - The
colorprop sets the SVGstrokeattribute, notfill. Lucide icons are stroke-based, sofillhas no visible effect on most icons. - If you use
classNameto set color via Tailwind (e.g.,text-blue-500), do not also set thecolorprop, as the explicit prop overridescurrentColor. sizesets bothwidthandheightsimultaneously. For non-square sizing, usewidthandheightprops 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
| Approach | Pros | Cons |
|---|---|---|
| Lucide React | Excellent tree-shaking, large icon set, active maintenance | Stroke-only style may not fit all designs |
| React Icons | Multiple icon families in one package | Larger install size, inconsistent APIs across families |
| Heroicons | Official Tailwind Labs project, great Tailwind integration | Smaller icon set (around 300 icons) |
| Custom SVG components | Full design control, zero dependencies | Manual 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 CSScolorproperty.
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
iconsobject (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
colorprop overridescurrentColor. - The Tailwind class (e.g.,
text-blue-500) setscurrentColor, 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
colorprop controls thestrokeattribute, notfill. - 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
sizeprop sets bothwidthandheightto the same value. - For non-square sizing, pass
widthandheightprops individually instead ofsize.
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
useStateto track which tool is active. - Each button has
aria-labelfor screen readers andaria-pressedto 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.