Icon Menu
A controlled popover menu with searchable icon factories, a virtualized grid, and upload support.
Installation
pnpm add @notion-kit/uiExamples
With IconBlock
Use the default factories for emoji and Lucide icons. Search runs through the autocomplete internals and each factory provides the searchable item text.
Notion Icons
Compose factories to choose which tabs appear. This example includes emoji, Lucide, and Notion-style icons.
Custom Factory
Use useCustomFactory to add your own icon set. name and keywords are searchable, and selected icons are tracked as recent items.
Anatomy
<IconMenu>
<IconBlock />
</IconMenu>For custom sources, pass factory results to factories:
const emoji = useEmojiFactory();
const brands = useCustomFactory({ icons });
<IconMenu factories={[emoji, brands]}>
<IconBlock icon={icon} />
</IconMenu>Factories
Icon Menu uses a factory pattern that lets each source define its tab, searchable items, grid sections, optional toolbar controls, and optional section navigation.
| Factory | Description |
|---|---|
useEmojiFactory() | Built-in emoji picker with skin tone support. |
useLucideFactory() | Full Lucide icon set with color picking. |
useNotionIconsFactory() | Notion-style icons (outline/solid in 10 colors). |
useCustomFactory() | Bring your own icon set via an icons array. |
useUploadFactory() | Persists user-uploaded icons in localStorage. |
API Reference
IconMenu
| Prop | Type | Default | Description |
|---|---|---|---|
factories | IconFactoryResult[] | - | Icon factory hooks to use. Falls back to built-in defaults when omitted. |
disabled | boolean | - | Whether the menu is disabled. |
onSelect | (icon: IconData) => void | - | Handler that is called when an icon or emoji is selected or when a URL is submitted. |
onUpload | (file: File) => void | - | Handler that is called when an image file is submitted. |
onRemove | () => void | - | Handler that is called when the remove button is clicked. |
- See
IconData
IconFactoryResult
| Field | Type | Description |
|---|---|---|
id | string | Unique factory id used for the tab value and storage keys. |
label | string | Tab label shown in the menu. |
sections | IconSection[] | Virtualized grid sections. Each section renders a label row. |
getItem | (id: string) => IconItem | Resolves an icon id into the item rendered by the grid. |
select | (id: string) => void | Optional selection side effect, commonly used for recents. |
search | (query: string) => string[] | Returns matching icon ids for the current query. |
toIconData | (item: IconItem) => IconData | Converts a selected item into the value passed to onSelect. |
renderIcon | (item: IconItem) => React.ReactNode | Renders the visual icon inside the grid button. |
renderToolbar | () => React.ReactNode | Optional controls shown beside the search input. |
renderNavigation | (scrollToSection: (id: string) => void, activeSectionId: string | null) => ReactNode | Optional section navigation for large factories. |
isLoading | boolean | Shows a searching status in the grid. |
hidden | boolean | Hides the factory tab, useful for empty upload history. |
getRandomIcon | () => IconItem | Optional random selection source for the toolbar action. |
Creating a Custom Factory
Use useCustomFactory to register your own icon set with the menu.
1. Define your icons
Each icon needs an id, name, url, and optional keywords for search:
const brands = useCustomFactory({
id: "brands",
label: "Brands",
icons: [
{
id: "github",
name: "GitHub",
url: "https://cdn.simpleicons.org/github/white",
keywords: ["git", "code", "repo"],
},
{
id: "slack",
name: "Slack",
url: "https://cdn.simpleicons.org/slack",
keywords: ["chat", "messaging"],
},
],
});2. Pass factories to IconMenu
Combine your factory with built-in ones and pass them via the factories prop:
const emoji = useEmojiFactory();
const brands = useCustomFactory({
id: "brands",
label: "Brands",
icons,
});
<IconMenu factories={[emoji, brands]} onSelect={setIcon}>
<IconBlock icon={icon} size="lg" />
</IconMenu>;3. Upload factory
Use useUploadFactory to let users submit icons via URL or file upload. Uploaded icons are persisted in localStorage:
const emoji = useEmojiFactory();
const upload = useUploadFactory();
<IconMenu
factories={[emoji, upload]}
onSelect={setIcon}
onUpload={(file) => setIcon({ type: "url", src: URL.createObjectURL(file) })}
>
<IconBlock icon={icon} size="lg" />
</IconMenu>;Factory options
| Option | Type | Description |
|---|---|---|
id | string | Unique ID for the factory tab. |
label | string | Tab label shown in the menu. |
icons | CustomIcon[] | Array of icon definitions. |
recentLimit | number | Max recent icons to track (default: 20). |