@arraypress/lucide-icon-picker
v1.0.0
Published
Searchable, categorized icon picker for the full Lucide icon library. Popover + modal browser variants, full-text search across name and tags, React components shared between admin and storefront.
Maintainers
Readme
@arraypress/lucide-icon-picker
Searchable, categorized icon picker for the full Lucide icon library. Ships two variants — a compact popover for forms and a full-screen modal browser for exploratory picking — plus a DynamicIcon renderer that works on both the admin and storefront sides of an app.
- ~1,695 icons indexed from
lucide-static, mapped to 17 top-level categories (Commerce, Media, Devices, Weather, etc.) by intersecting each icon's tags against curated keyword lists. Zero hand-mapping, so the index regenerates cleanly when Lucide adds new icons. - Full-text search matches both the icon name and its tags — typing
"delivery"surfaces truck, package, box, shipping, etc. - Legacy-name tolerant — reads PascalCase values (
FolderOpen,BarChart3) from older pickers without requiring a DB migration. - Stateless, zero shadcn coupling — uses bare
@radix-ui/react-popover+@radix-ui/react-dialog, so it drops into any Tailwind + Radix setup.
Install
npm install @arraypress/lucide-icon-pickerPeer dependencies:
react >= 18lucide-react >= 1.7@radix-ui/react-popover >= 1(optional — only for<IconPicker>)@radix-ui/react-dialog >= 1(optional — only for<IconBrowser>)
The /react subpath entry is where the picker components live. The main entry point ships only the data index + the DynamicIcon renderer with no Radix dependency — ideal for the storefront side of a split admin/store app.
Usage
Rendering icons (any app)
import { DynamicIcon } from '@arraypress/lucide-icon-picker';
function CollectionCard({ collection }) {
return (
<div>
<DynamicIcon
name={collection.icon} // 'truck' OR legacy 'Truck'
className="size-5"
style={collection.color ? { color: collection.color } : undefined}
/>
<span>{collection.name}</span>
</div>
);
}DynamicIcon returns null for empty or unknown icon names, so you don't need a guard at the call site.
Popover picker — <IconPicker>
Compact, form-embedded. Use this inside settings forms, flyouts, toolbars — anywhere a modal would feel heavy.
import { IconPicker } from '@arraypress/lucide-icon-picker/react';
function CollectionEditor({ collection, onChange }) {
return (
<IconPicker
value={collection.icon}
onChange={(slug) => onChange({ ...collection, icon: slug })}
color={collection.color}
/>
);
}Modal browser — <IconBrowser>
Full-screen three-panel dialog: search header, left category sidebar, right icon grid at 40px/cell. Use this when the merchant wants to explore the full library before deciding.
import { IconBrowser } from '@arraypress/lucide-icon-picker/react';
function ThemeSettings({ theme, setTheme }) {
return (
<IconBrowser
value={theme.brandIcon}
onChange={(slug) => setTheme({ ...theme, brandIcon: slug })}
title="Choose a brand icon"
/>
);
}Helpers
import {
toSlug,
isValidIcon,
searchIcons,
ICON_CATEGORIES,
ICONS_BY_CATEGORY,
ICON_META,
ALL_ICONS,
} from '@arraypress/lucide-icon-picker';
// Normalize legacy PascalCase names to kebab-case slugs
toSlug('FolderOpen'); // 'folder-open'
toSlug('BarChart3'); // 'bar-chart-3'
// Check an icon exists
isValidIcon('truck'); // true
isValidIcon('bad-name'); // false
// Full-text search across name + tags
searchIcons('delivery'); // ['truck', 'package', 'box', ...]
// Category listing
ICON_CATEGORIES; // ['Arrows & Navigation', 'Commerce & Money', ...]
ICONS_BY_CATEGORY['Commerce & Money']; // ['banknote', 'cart', 'coin', ...]
// Per-icon metadata
ICON_META['truck']; // { pascal: 'Truck', category: 'Transport & Delivery', tags: [...] }API reference
<IconPicker>
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | — | Current icon slug (or legacy PascalCase name). |
| onChange | (slug: string) => void | — | Called with the new slug on selection, or "" on clear. |
| color | string | — | Accent color applied to the selected icon in the trigger + grid. |
| trigger | ReactNode | Outlined button | Custom trigger element. |
| align | 'start' \| 'center' \| 'end' | 'start' | Popover alignment. |
| sideOffset | number | 4 | Popover side offset in px. |
| placeholder | string | 'No icon' | Label shown when no icon is selected. |
<IconBrowser>
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | — | Current icon slug (or legacy PascalCase name). |
| onChange | (slug: string) => void | — | Called with the new slug on selection, or "" on clear. |
| color | string | — | Accent color applied to the selected icon. |
| trigger | ReactNode | Outlined button | Custom trigger element. |
| title | string | 'Choose an icon' | Dialog title. |
| placeholder | string | 'No icon' | Label shown when no icon is selected. |
<DynamicIcon>
| Prop | Type | Description |
|------|------|-------------|
| name | string | Icon slug (truck) or legacy PascalCase name (Truck). |
| className | string | CSS class applied to the SVG. |
| style | CSSProperties | Inline style applied to the SVG. |
| size | number \| string | Icon size (width + height). |
| strokeWidth | number \| string | Stroke width. |
| color | string | Icon color override. |
Value format
Stored values are kebab-case slugs matching lucide-static's convention:
truck
folder-open
bar-chart-3Legacy PascalCase names (Truck, FolderOpen, BarChart3) are automatically normalized on read via toSlug(), so existing DB rows written by older pickers keep rendering without a migration. New values written by this picker are always kebab-case.
Regenerating the icon index
The category index is checked into src/icon-index.js and baked at build time so consumers don't need lucide-static at runtime. Regenerate after bumping lucide-static:
npm run build:indexThe curated category keyword lists live in scripts/build-index.mjs. Each icon's tags are intersected against these keyword lists — first match wins. Icons with no matching keywords fall into a Misc bucket.
License
MIT © David Sherlock
