Skip to content
Component installs need the registry setup
VegaStack Design

Emoji Picker

A popover with a quick row, search, categories, recents and a lazily loaded emoji grid that returns the selected character.

Status
stable
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-picker

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/emoji-picker

Run 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 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

PropTypeDefaultDescription
onValueChange*(emoji: string) => void—Called with the selected emoji character when the user picks one. The popover closes after selection (unless closeOnSelect is false).
alignAlign"start"Alignment of the panel relative to the trigger.
classNamestring—Extra classes for the popover panel.
closeOnSelectbooleantrueClose the popover automatically after an emoji is selected.
footerReact.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).
openboolean—Controlled open state of the popover. Omit for uncontrolled usage.
preferenceKeystring—Account/workspace-scoped recent preference namespace.
quickEmojistring[]—A row of one-click emoji above the search — the quick reactions of a reaction bar.
refReact.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).
searchPlaceholderstring"Search emoji"Placeholder and accessible label for the search field.
showRecentsbooleantrueShow the "Recent" section: the viewer's last picks, kept in localStorage.
sideSide"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.
triggerReact.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.
triggerLabelstring"Pick an emoji"Accessible label for the default trigger button.
valuestring—Current emoji for identity selection styling.

Data attributes and CSS variables on EmojiPicker

AttributeValues
data-sizemirrors 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.

PropTypeDefaultDescription
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.
keywordsstring[]—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 an aria-label (the emoji name) and title, 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 polite role="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.
KeyAction
Enter / SpaceOpen 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 / EndJump to the first or last emoji.
Tab / Shift + TabMove between the search field and the grid — the grid is one tab stop.
EscClose the picker and return focus to the trigger.
ContractStates tested
Behaviourdefault, active, empty, filtering, open, selected, loading, recents
Accessibilityfocus-visible, keyboard, labeled, live, status-announcement, semantic-html
Visualdefault, hover, focus, empty

Do / Don't

Do
Use it for lightweight emoji insertion — reactions, comments, status, icon selection. Keep ownership of the value in the parent via onValueChange.
Don't
Rely on it as a complete Unicode emoji browser — it ships a curated subset. Use a dedicated library if you need full coverage.

On this page