Emoji Picker
A popover with a quick row, search, categories, recents and a lazily loaded emoji grid that returns the selected character.
- Status
- Since
0.1.0- Accessibility pattern
- APG dialog (non-modal) picker
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Emoji Picker before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify emoji-pickerInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/emoji-pickerRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/button, @vegastack/emoji-data, @vegastack/picker-panel, @vegastack/picker-preferences, @vegastack/popover.
Usage
import { EmojiPicker } from "@/components/ui/emoji-picker";
<EmojiPicker onValueChange={(emoji) => console.log(emoji)} />;How it works
EmojiPicker composes Popover (trigger + floating panel), the shared
panel-search row — a leading glyph and a bare text field, because a bordered input inside a
bordered popup would nest two borders — and a scrollable grid of icon
Buttons. That row is a
shared internal this picker and ShortcutOverlay install; it arrives
automatically and is documented there. SearchableSelect draws its own search field and does not
install it.
Selecting an emoji fires onValueChange(emoji) with the character and (by default) closes the panel.
Above the grid sit an optional quick row (quickEmoji), the search and a category bar that jumps
to a section. The first section, "Recent", holds the viewer's last picks, kept in localStorage
(showRecents={false} turns it off). size="sm" is the compact panel.
The data is a curated set of about 260 emoji in nine categories, in the
emoji-data lib. The picker loads it with a dynamic import() the
first time it opens and shows a skeleton grid meanwhile, so a page that only renders the trigger
never ships the table. useEmojiData(enabled) gives you the same module, for example to name an
emoji. It is not the full Unicode set; skin tones are not offered.
Examples
Insert into a field
Append the picked emoji to a text input — the picker is presentational, so the parent owns the value.
Custom trigger
Pass a button-like element as trigger (composed via Base UI's render) to replace the default ghost
icon button. The forwarded ref lands on the trigger button.
Multi-pick (keep open)
Set closeOnSelect={false} so the panel stays open after each selection — useful for
building a reaction bar or inserting several emoji in a row.
<EmojiPicker
closeOnSelect={false}
onValueChange={(emoji) => setReactions((prev) => [...prev, emoji])}
/>Side and labels
Use side to place the panel relative to the trigger, and triggerLabel /
searchPlaceholder to customise the trigger's aria-label and the search placeholder copy.
<EmojiPicker
side="right"
triggerLabel="Insert symbol"
searchPlaceholder="Find a symbol…"
onValueChange={insert}
/>Custom panel class
className is forwarded to the popover panel, so you can override its width (or other layout)
without touching the trigger. The default panel is sized to its grid (w-79, w-62.5 when
small, the same widths as IconPicker); here it is widened to w-96.
<EmojiPicker className="w-96" onValueChange={insert} />Quick row
quickEmoji puts one-click emoji above the search. Reactions
passes 👍 ❤️ 😄 🎉 👀 🙏.
Compact
size="sm" is a narrower panel with 28px cells.
Without recents
Footer
footer adds a row at the bottom of the panel, under a divider, for an action that belongs with
the picker — "Remove icon" on a space or page icon. Its controls are yours: close the picker from
them with open / onOpenChange.
Empty state
When the search query matches nothing, the grid is replaced by an empty state, "No emoji found", and a polite role="status" region announces the result count (or the empty state)
as the query changes. This is owned by the picker's internal search state, so the example
below is live — open it and type a non-matching query like zzz.
Open the picker and type zzz in the search field to see the empty state.
API Reference
EmojiPicker
| Prop | Type | Default | Description |
|---|---|---|---|
onValueChange* | (emoji: string) => void | — | Called with the selected emoji character when the user picks one. The popover closes after
selection (unless closeOnSelect is false). |
align | Align | "start" | Alignment of the panel relative to the trigger. |
className | string | — | Extra classes for the popover panel. |
closeOnSelect | boolean | true | Close the popover automatically after an emoji is selected. |
footer | React.ReactNode | — | A row at the bottom of the panel, under a divider — "Remove icon", say. Its controls are
the host's; close the picker from them with open / onOpenChange when they act. |
onOpenChange | ((open: boolean) => void) | — | Called when the popover's open state changes (controlled or uncontrolled). |
open | boolean | — | Controlled open state of the popover. Omit for uncontrolled usage. |
preferenceKey | string | — | Account/workspace-scoped recent preference namespace. |
quickEmoji | string[] | — | A row of one-click emoji above the search — the quick reactions of a reaction bar. |
ref | React.Ref<HTMLButtonElement> | — | Ref forwarded to the trigger button — the component's focusable root (the popover panel is portaled, so the trigger is the stable host element to focus/measure). |
searchPlaceholder | string | "Search emoji" | Placeholder and accessible label for the search field. |
showRecents | boolean | true | Show the "Recent" section: the viewer's last picks, kept in localStorage. |
side | Side | "bottom" | Which side of the trigger to place the panel on. |
size | "default" | "sm" | "default" | sm is the compact panel (narrower, 28px cells) for tight spots such as a comment's actions. |
trigger | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | — | Custom trigger element, composed via Base UI's render prop. Defaults to a ghost icon button
with a smiley-plus glyph. |
triggerLabel | string | "Pick an emoji" | Accessible label for the default trigger button. |
value | string | — | Current emoji for identity selection styling. |
Data attributes and CSS variables on EmojiPicker
| Attribute | Values |
|---|---|
data-size | mirrors a prop or state value |
data-slot | "emoji-picker" | "emoji-picker-footer" |
EmojiEntry
One emoji in the emoji-data set — the character, its accessible name and search keywords.
| Prop | Type | Default | Description |
|---|---|---|---|
char* | string | — | The emoji character to render and return from onValueChange. |
name* | string | — | Human-readable name — used as the button aria-label and matched by search. |
keywords | string[] | — | Extra search terms (beyond name) that should surface this emoji. |
EmojiCategory
"Smileys" | "People" | "Animals" | "Food" | "Activities" | "Travel" | "Objects" | "Symbols" | "Flags"
— the section order of the grid and the category bar. The data itself lives in the emoji-data lib.
Accessibility
- The trigger is a real
<button>; Enter / Space open the popover and focus moves into the panel. - Each emoji is a focusable
<button>with anaria-label(the emoji name) andtitle, so the grid is screen-reader navigable and keyboard-operable. - The search field is a labelled
<input type="search">; typing filters the grid live, and a politerole="status"announces result counts or the empty state when nothing matches. - Focus is a background tint from the tokens'
base.css, on the trigger and every emoji button alike; there is no ring. The search row is text entry: while the field holds focus its bottom hairline darkens subtly (border-ring/50, easing in over 150ms) and nothing fills — FOC-14's text-entry exception (MK 2026-09-27). - While the data loads, the skeleton is hidden from assistive technology.
- Dismiss is built in (via
Popover) — clicking outside the panel or pressing Esc closes it and returns focus to the trigger.
| Key | Action |
|---|---|
| Enter / Space | Open the picker (on the trigger) or select the focused emoji. |
| ← / → | Move one emoji within the grid, the quick row or the category bar. |
| ↑ / ↓ | Move one row in the grid. |
| Home / End | Jump to the first or last emoji. |
| Tab / Shift + Tab | Move between the search field and the grid — the grid is one tab stop. |
| Esc | Close the picker and return focus to the trigger. |
| Contract | States tested |
|---|---|
| Behaviour | default, active, empty, filtering, open, selected, loading, recents |
| Accessibility | focus-visible, keyboard, labeled, live, status-announcement, semantic-html |
| Visual | default, hover, focus, empty |