Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Emoji Picker

A popover with a searchable, category-grouped grid of emoji that returns the selected character.

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

The 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

PropTypeDefaultDescription
onValueChange*(emoji: string) => voidCalled 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.
classNamestringExtra classes for the popover panel.
closeOnSelectbooleantrueClose the popover automatically after an emoji is selected.
onOpenChange((open: boolean) => void)Called when the popover's open state changes (controlled or uncontrolled).
openbooleanControlled open state of the popover. Omit for uncontrolled usage.
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 for the search input.
sideSide"bottom"Which side of the trigger to place the panel on.
triggerReact.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.
triggerLabelstring"Pick an emoji"aria-label for the default trigger button.

Data attributes and CSS variables on EmojiPicker

AttributeValues
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

PropTypeDefaultDescription
char*stringThe emoji character to render and return from onValueChange.
name*stringHuman-readable name — used as the button aria-label and matched by search.
keywordsstring[]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.)

PropTypeDefaultDescription
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.

PropTypeDefaultDescription
EMOJIRecord<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 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 always visible: the default ghost trigger keeps the browser's native focus outline, the search field re-colours its border to the ring token (focus:border-ring/(--alpha-tint-border)), and each emoji button highlights with a focus-visible:bg-accent background — never outline: none.
  • 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.
Tab / Shift + TabMove focus between the search field and emoji buttons.
EscClose the picker and return focus to the trigger.
ContractStates tested
Behaviourdefault, active, empty, filtering, open, selected
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. Extend the EMOJI dataset if you need full coverage, or use a dedicated library.

On this page