Icon Menu

A controlled popover menu with searchable icon factories, a virtualized grid, and upload support.

Installation

pnpm add @notion-kit/ui

Examples


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.

FactoryDescription
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

PropTypeDefaultDescription
factoriesIconFactoryResult[]-Icon factory hooks to use. Falls back to built-in defaults when omitted.
disabledboolean-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.

IconFactoryResult

FieldTypeDescription
idstringUnique factory id used for the tab value and storage keys.
labelstringTab label shown in the menu.
sectionsIconSection[]Virtualized grid sections. Each section renders a label row.
getItem(id: string) => IconItemResolves an icon id into the item rendered by the grid.
select(id: string) => voidOptional selection side effect, commonly used for recents.
search(query: string) => string[]Returns matching icon ids for the current query.
toIconData(item: IconItem) => IconDataConverts a selected item into the value passed to onSelect.
renderIcon(item: IconItem) => React.ReactNodeRenders the visual icon inside the grid button.
renderToolbar() => React.ReactNodeOptional controls shown beside the search input.
renderNavigation(scrollToSection: (id: string) => void, activeSectionId: string | null) => ReactNodeOptional section navigation for large factories.
isLoadingbooleanShows a searching status in the grid.
hiddenbooleanHides the factory tab, useful for empty upload history.
getRandomIcon() => IconItemOptional 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

OptionTypeDescription
idstringUnique ID for the factory tab.
labelstringTab label shown in the menu.
iconsCustomIcon[]Array of icon definitions.
recentLimitnumberMax recent icons to track (default: 20).