Emoji pickerPro

A full emoji picker with instant search, scroll-synced categories, skin tones, recently used emoji, and a virtualized, keyboard-navigable grid.

Live · keyboard ready
  1. Jordan Reyes

    Launch checklist is green. Shipping the pricing page at 3pm unless anyone objects.

  2. Chloe Nguyen

    Copy is final and the screenshots are swapped in.

Shift picks several
  • Message composers, comment boxes, and chat inputs.
  • Choosing an emoji for a status, a project, a page icon, or a label.
  • Any text field where people expect the full emoji set with search.
  • Use reactions for a small, fixed set of quick responses on a post or comment.
  • Use combobox or a select when the choices are a short, known list.
  • Do not use it as an icon picker; emoji render differently on every platform.

Installation

Pro source and install commands unlock with a Pro plan.

example.tsx
import { EmojiPicker } from "@/components/arc/emoji-picker/emoji-picker";import { Popover, PopoverContent, PopoverTrigger } from "@/components/arc/popover/popover"; export function EmojiButton({ onInsert }: { onInsert: (emoji: string) => void }) {  const [open, setOpen] = useState(false);  return (    <Popover open={open} onOpenChange={setOpen}>      <PopoverTrigger aria-label="Add emoji"><SmilePlus /></PopoverTrigger>      <PopoverContent side="top" style={{ padding: 0, width: "min(22rem, calc(100vw - 20px))" }}>        <EmojiPicker          onSelect={(selection, event) => {            onInsert(selection.emoji);            if (!event.shiftKey) setOpen(false);          }}        />      </PopoverContent>    </Popover>  );}

EmojiPicker

A full emoji picker: instant search over names and keywords, category tabs whose indicator follows your scroll, skin tones, recently used emoji remembered on the device, a preview row with the emoji name, and a virtualized grid that is fully usable from the keyboard. Ships its own dataset of every emoji up to Emoji 15.0. It has no chrome of its own, so it drops into a popover, sheet, or card.

PropTypeDefaultDescription
onSelect(selection: EmojiSelection, event: MouseEvent | KeyboardEvent) => void–Called when an emoji is chosen by click, Enter, or Space. selection has emoji (with the skin tone applied), base, name, and skinTone. Check event.shiftKey to keep a popover open for several picks.
skinTone0 | 1 | 2 | 3 | 4 | 5–Controlled skin tone: 0 is the default yellow, 1 to 5 go from light to dark.
defaultSkinTone0 | 1 | 2 | 3 | 4 | 5–Skin tone before anything has been stored.
onSkinToneChange(tone: SkinTone) => void–Called when a tone is chosen in the tone menu.
storageKeystring | null"arc-emoji-picker"localStorage key for recent emoji and the tone. Pass null to keep them in memory for this page only. Blocked storage falls back to memory automatically.
recentLimitnumber27How many recently used emoji to keep.
defaultRecentstring[]18 popular emojiShown under Popular until something has been picked.
columnsnumber–Emoji per row. Defaults to as many 36px cells as fit, at least 6.
autoFocusbooleanfalseFocuses the search field on mount. Popovers and dialogs usually focus it already.
searchPlaceholderstring"Search emoji"Placeholder of the search field.
labelstring"Emoji picker"Accessible name of the picker group.
classNamestring–Class on the root. Set --emoji-picker-width and --emoji-picker-height to size it (352px by 408px by default).
styleCSSProperties–Styles on the root.
Type
Filters every emoji by name and keyword; the best match is ready for Enter. Typing while the grid has focus goes to the search field.
Enter
In the search field, picks the highlighted emoji. In the grid, picks the active emoji.
Arrow Down
From the search field, moves into the grid.
Arrow keys
Move through the grid. Up and down keep the column across sections; Up from the first row returns to search.
HomeorEnd
First or last emoji in the row. With Ctrl or Cmd, first or last emoji overall.
Page UporPage Down
Moves a screen of rows.
Space
Picks the active emoji in the grid.
Arrow LeftorArrow Right
In the category bar, moves between categories; Enter jumps to one. In the tone menu, moves between tones.
Escape
Closes the tone menu, then clears the search, then closes the surrounding popover.
  • The grid is role="grid" with rows and gridcells named after each emoji; section names are column headers. The active cell is exposed with aria-activedescendant from the search field while searching, and from the grid while it has focus, so the active emoji is announced as it moves.
  • Only the rows in view are rendered, plus the active row, so aria-activedescendant always points at a real element. Rows carry aria-rowindex and the grid aria-rowcount.
  • The number of results is announced in a polite live region while searching, including when nothing matches.
  • The category bar is a toolbar with one tab stop; the category in view is marked with aria-current. The tone control is a button with aria-expanded that opens a radiogroup of six named tones.
  • Escape is handled on window in the capture phase and marked as handled only when it closes the tone menu or clears the search, so the first Escape never closes a surrounding Radix popover by accident.
  • The preview row repeats the active emoji's name for sighted users and is hidden from assistive technology.
  • The category indicator slides to the category under the top edge on the morph spring as you scroll. When a tab is pressed it goes straight there and waits while the list scrolls smoothly to the category, instead of stepping through every category in between.
  • The skin tone menu unrolls leftward out of the tone button on a clip-path inset, and rolls back into it; the inset is a CSS transition, so a quick close reverses mid-way.
  • The preview emoji settles in from a slight scale on each change; a pressed emoji dips and springs back.
  • Search results appear instantly, with no animation, so typing always feels immediate.
  • With reduced motion the indicator and tone menu move at once, scrolling to a category jumps, and the preview does not scale.
  • The grid picks its column count from the width, keeping cells at least 36px, so it goes from 9 columns at 352px to 7 in a narrow phone popover.
  • The default size is 352 by 408px and it never exceeds its container; set --emoji-picker-width and --emoji-picker-height to change it.
  • Hover previews only follow a mouse pointer; on touch a tap picks the emoji directly.
  • The dataset is about 73 kB of compact strings (1,870 emoji with names, keywords, and skin tone positions) parsed once on first use, then shared by every picker on the page.
  • The grid is virtualized: only the rows in view plus a small overscan are rendered, so scrolling the whole set stays light.
  • Search scans precomputed lowercase strings and is memoized per query.
  • Skin tones are applied when a cell renders, so there is no second dataset for toned variants.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Put it in a popover next to the input it fills. Remember the input's selection and insert at it, then move the caret after the emoji; see the composer demo.
  • The picker has no border, background, or padding of its own. Give the popover padding: 0 so the grid runs edge to edge.
  • Keep the popover open when Shift is held for several picks in a row, and close it otherwise.
  • The dataset covers Emoji 15.0, which renders on current Apple, Google, and Microsoft platforms. Older systems may show a few newer emoji as boxes.
  • Use onSkinToneChange with skinTone to sync the tone with a user profile; otherwise it is remembered in localStorage with the recent emoji.

The full library index for assistants is at /llms.txt.