Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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>
);
}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>
);
}lucide-react, but also available as a standalone module at lucide-react/dist/esm/icons/{icon-name}.currentColor stroke.className, style, onClick, and ARIA attributes.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>
);
}LucideIcon type to type icon components passed as props.LucideProps, which extends SVGProps<SVGSVGElement> with size, color, strokeWidth, and absoluteStrokeWidth.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;
}lucide-react with barrel imports like import * as icons will bundle every icon (over 1,400 SVGs). Always use named imports for production builds.color prop sets the SVG stroke attribute, not fill. Lucide icons are stroke-based, so fill has no visible effect on most icons.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.icons[name] provide no compile-time validation that the icon name exists. Consider building a whitelist for CMS-driven scenarios.
| 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 |
npm install lucide-react
import { Search } from "lucide-react";
export function MyComponent() {
return <Search size={24} className="text-gray-600" />;
}currentColor, which inherits from the parent CSS color property.icons object (import { icons } from "lucide-react") bundles all 1,400+ icons and defeats tree-shaking.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.
color prop overrides currentColor.text-blue-500) sets currentColor, but the prop takes precedence.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>
);
}color prop controls the stroke attribute, not fill.fill="red" on a stroke icon will not change its appearance.size prop sets both width and height to the same value.width and height props individually instead of size.import type { LucideIcon, LucideProps } from "lucide-react";
interface Props {
icon: LucideIcon;
iconProps?: LucideProps;
}LucideIcon types the component itself; LucideProps extends SVGProps<SVGSVGElement>.
useState to track which tool is active.aria-label for screen readers and aria-pressed to indicate toggle state.strokeWidth (2.5 vs 2) and a blue background.icons[name] provides no compile-time validation that the icon name exists.undefined.Record<string, LucideIcon>.Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥