Emoji Picker
ProA complete picker: searchable, with a scroll-spy category nav, a pinned frequently-used row, persisted recents and a hovered preview. Categories mount lazily, so a large set never paints at once.
Installation#
Usage#
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Anatomy#
import {
EmojiPicker,
EmojiPickerPopover,
EmojiPickerTrigger,
EmojiPickerContent,
} from "@/components/ui/emoji-picker"
<EmojiPicker />
<EmojiPickerPopover>
<EmojiPickerTrigger />
<EmojiPickerContent />
</EmojiPickerPopover>In a popover#
The most common flow: a trigger opens the picker in a Radix popover that closes on select. Compose EmojiPickerPopover, EmojiPickerTrigger (use asChild to keep your own button), and EmojiPickerContent.
Recents & presets#
The picker tracks a recently used row automatically and persists it to localStorage (set storageKey, or pass null to disable). For full control, drive recent / onRecentChange yourself. The presets row is a fixed quick-pick list pinned above the categories; override it with the presets prop.
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Density#
Density retunes the panel width, viewport height, grid gap, and glyph size, never color or radius. compact is the default, sized for application UI; comfortable opens it up for marketing pages. Without a prop it resolves from the nearest DensityProvider.
Minimal#
Every region is toggleable. Drop the preview footer and the recents/presets rows with showPreview, showRecent, and showPresets for a lean search-and-grid.
Performance#
Categories mount in batches. Only categoriesPerPage are in the DOM at first, and a bottom sentinel reveals the next batch as you scroll (search results stream the same way). Counts only ever grow, so scroll height never jitters. Lower the value for a lighter first paint, or pass your own fuller dataset via data without changing the component.
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
API reference#
EmojiPicker
The whole picker: search, category nav, grid and preview footer. Forwards all native <div> props except onSelect; every part accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| onEmojiSelect | (emoji: string, datum: EmojiDatum) => void | - | Called with the glyph and its { char, name, keywords } record when an emoji is chosen. |
| data | EmojiCategory[] | emojiCategories | The categorized dataset; swap in a full Unicode export without touching the component. |
| presets | string[] | defaultEmojiPresets | The fixed frequently-used row pinned above the categories. |
| recent | string[] | - | Controlled recently-used list, most recent first. |
| defaultRecent | string[] | [] | Initial recents when uncontrolled; replaced by the persisted list once it loads. |
| onRecentChange | (recent: string[]) => void | - | Called whenever the recents change, controlled or not. |
| recentLimit | number | 24 | Most recents kept. |
| storageKey | string | null | "koala-emoji-recent" | localStorage key that persists uncontrolled recents; null turns persistence off. |
| showSearch | boolean | true | Render the search row. |
| showPreview | boolean | true | Render the footer that previews the hovered or focused emoji. |
| showRecent | boolean | true | Render and track the recently-used row; off, picks are not recorded. |
| showPresets | boolean | true | Render the presets row. |
| categoriesPerPage | number | 3 | Categories mounted at first and per scroll batch; search results stream in batches of this times 16. |
| searchPlaceholder | string | "Search emoji" | Placeholder of the search field. |
| recentLabel | string | "Recently used" | Header and nav label of the recents row. |
| presetsLabel | string | "Frequently used" | Header and nav label of the presets row. |
| previewPlaceholder | string | "Pick an emoji" | Footer hint before anything is hovered or focused. |
| density | "comfortable" | "compact" | "compact" | Width (352px or 320px), viewport height, grid gap and glyph size. Falls back to the nearest DensityProvider. |
| Data attribute | Description |
|---|---|
| data-active | true on the category nav button whose section is at the top of the viewport. |
| CSS variable | Description |
|---|---|
| --surface | Set to var(--popover), so the nested search Input blends with the panel. |
EmojiPickerPopover
The popover root, Radix Popover.Root; renders no DOM and lets the content close itself on select.
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | - | Controlled open state. |
| defaultOpen | boolean | false | Initial open state when uncontrolled. |
| onOpenChange | (open: boolean) => void | - | Called when the popover opens or closes, including the close after a pick. |
| modal | boolean | false | Block outside interaction and trap focus while open. |
EmojiPickerTrigger
The button that opens the picker, Radix Popover.Trigger. Renders a <button> and forwards all its props.
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | false | Merge onto your own button instead of rendering one. |
| Data attribute | Description |
|---|---|
| data-state | "open" or "closed". |
EmojiPickerContent
Radix Popover.Content portalled to the body, holding an EmojiPicker. Radix Content props (placement, collisions, focus and dismiss callbacks) go to the popover; every other prop, EmojiPicker options and native div props alike, goes to the picker. className styles the popover wrapper; pickerClassName styles the picker.
| Prop | Type | Default | Description |
|---|---|---|---|
| closeOnSelect | boolean | true | Close the popover after a pick. |
| pickerClassName | string | - | Classes for the inner picker surface. |
| side | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side of the trigger; flips on collision. |
| sideOffset | number | 8 | Gap in pixels between the trigger and the picker. |
| align | "start" | "center" | "end" | "start" | Alignment against the trigger. |
| alignOffset | number | 0 | Offset in pixels from the align edge. |
| avoidCollisions | boolean | true | Flip and shift to stay inside the viewport. |
| collisionPadding | number | Partial<Record<Side, number>> | 0 | Distance from the boundary edges that counts as a collision. |
| collisionBoundary | Element | Element[] | null | [] | Extra elements used as the collision boundary. |
| sticky | "partial" | "always" | "partial" | Keep the picker in view while the trigger scrolls. |
| hideWhenDetached | boolean | false | Hide the picker when the trigger is fully clipped. |
| onOpenAutoFocus | (event: Event) => void | - | Fires when focus moves into the picker on open; preventDefault() to keep it on the trigger. |
| onCloseAutoFocus | (event: Event) => void | - | Fires when focus returns to the trigger on close. |
| onEscapeKeyDown | (event: KeyboardEvent) => void | - | Fires on Escape; preventDefault() keeps it open. |
| onInteractOutside | (event: Event) => void | - | Fires on a pointer or focus outside; preventDefault() keeps it open. onPointerDownOutside and onFocusOutside are also accepted. |
| forceMount | true | - | Keep the popover mounted while closed, for external animation control. |
| Data attribute | Description |
|---|---|
| data-state | "open" or "closed". |
| data-side | The side the popover actually opened on, after collisions; drives the slide direction. |
| data-align | "start", "center" or "end". |