Emoji Picker
A popover with a searchable, category-grouped grid of emoji that returns the selected character.
- Status
- Since
0.1.0- Accessibility pattern
- APG dialog (non-modal) picker
Last updated
Install
Add Emoji Picker from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/emoji-pickerThe same command installs the registry items it composes: @vegastack/floating-surface, @vegastack/icon-button, @vegastack/popover, @vegastack/use-list-nav.
Usage
import { EmojiPicker } from "@/components/ui/emoji-picker";
<EmojiPicker onValueChange={(emoji) => console.log(emoji)} />;How it works
EmojiPicker composes our Popover (trigger + floating panel), a search
Input, and a scrollable grid of icon Buttons.
Selecting an emoji fires onValueChange(emoji) with the character and (by default) closes the panel.
The emoji data is a curated, embedded set (EMOJI) — a few hundred of the most common emoji
grouped into nine categories: Smileys, People, Animals, Food, Activities, Travel, Objects, Symbols,
and Flags. It is intentionally not the full Unicode set: keeping the data inline makes the
component self-contained and zero-dependency (no heavy emoji library, no sprite sheets). Search
matches each emoji's name and keywords. To support more emoji, extend or replace the exported
EMOJI record.
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. Here the default w-72 panel is widened to w-80.
<EmojiPicker className="w-80" onValueChange={insert} />Empty state
When the search query matches nothing, the grid is replaced by a "No emoji found."
message 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. |
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. |
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 for the search input. |
side | Side | "bottom" | Which side of the trigger to place the panel on. |
trigger | React.ReactElement<Omit<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref">, string | React.JSXElementConstructor<any>> | — | Custom button-like trigger element (composed via Base UI render). Defaults to a ghost
SmilePlus icon button. |
triggerLabel | string | "Pick an emoji" | aria-label for the default trigger button. |
Data attributes and CSS variables on EmojiPicker
| Attribute | Values |
|---|---|
data-slot | "emoji-picker" | "emoji-picker-empty" | "emoji-picker-grid" | "emoji-picker-item" | "emoji-picker-search" | "emoji-picker-status" |
EmojiEntry
A single entry in the EMOJI dataset — the rendered character, its accessible name, and
optional search keywords.
EmojiEntry
| 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
The category keys of the EMOJI record, rendered as section headings in their declared
order. (EmojiCategory is a string-union type alias, so the values are listed by hand.)
| Prop | Type | Default | Description |
|---|---|---|---|
EmojiCategory | "Smileys" | "People" | "Animals" | "Food" | "Activities" | "Travel" | "Objects" | "Symbols" | "Flags" | — | One of the nine emoji categories. The keys of EMOJI, used as grid section headings and search groupings. |
EMOJI
The exported, curated emoji dataset you extend or replace to support more emoji. It is a
Record<EmojiCategory, EmojiEntry[]> — each category key maps to an ordered array of
EmojiEntry objects.
| Prop | Type | Default | Description |
|---|---|---|---|
EMOJI | Record<EmojiCategory, EmojiEntry[]> | — | Curated, embedded emoji dataset (~300 entries). Not the full Unicode set — kept inline so the component ships zero extra dependencies. Add entries or swap in your own data to extend coverage. |
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 always visible: the default ghost trigger keeps the browser's native focus outline, the search field re-colours its border to the
ringtoken (focus:border-ring/(--alpha-tint-border)), and each emoji button highlights with afocus-visible:bg-accentbackground — neveroutline: none. - 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. |
| Tab / Shift + Tab | Move focus between the search field and emoji buttons. |
| Esc | Close the picker and return focus to the trigger. |
| Contract | States tested |
|---|---|
| Behaviour | default, active, empty, filtering, open, selected |
| Accessibility | focus-visible, keyboard, labeled, live, status-announcement, semantic-html |
| Visual | default, hover, focus, empty |
Do / Don't
Color Picker
A swatch-triggered popover that presents a grid of preset colors — pick one, fire onValueChange, mark the selection with a check.
Floating Surface
The shared floating-overlay module — one Portal/Positioner/Popup composer, the popup surface recipes, the list-item recipe, and the in-panel search row.