# Emoji Picker

> A React emoji picker with search, category tabs, recent emoji, skin tones and a preview, bundling 1,870 emoji with names and keywords.

- Type: React component (Inputs)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add emoji-picker`
- Docs: https://wingo-ui.com/components/emoji-picker

## Interaction

Type in the search field and the grid narrows to matching emoji at once, best match first; Enter picks the first one and Escape clears the query. Click a category tab to jump to its section, and the tab under the scroll position lights up as you scroll. Hover an emoji to see it large in the footer with its name and shortcode. The hand button opens the six skin tones in the header row; the choice applies to every emoji that takes one and is remembered, like the recently used row at the top. Keyboard: Tab goes from the search to the skin tone button, the tabs (one stop, arrows move along) and the grid; in the grid the arrows move, Home and End go to the row ends, Page Up and Page Down jump five rows, Enter or Space picks, and typing any letter goes back to the search. Only the rows on screen are rendered. On a touch screen every cell is 44px, and EmojiPickerPopover opens as a bottom sheet the picker fills below 768px.

## Installation

```bash
npx wingo-ui@latest add emoji-picker
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/emoji-picker` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/emoji-picker.tsx`
- `components/ui/emoji-picker-data.ts`

npm dependencies: `npm i @tanstack/react-virtual lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Popover](https://wingo-ui.com/components/popover) ([Markdown](https://wingo-ui.com/components/popover.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [usePersistedState](https://wingo-ui.com/components/use-persisted-state) ([Markdown](https://wingo-ui.com/components/use-persisted-state.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { EmojiPicker, EmojiPickerPopover } from "@/components/ui/emoji-picker"

export function CommentBox() {
  const [text, setText] = useState("Congrats on the launch, Emma ")
  return (
    <div className="flex flex-col gap-3">
      <div className="flex items-center gap-2">
        <input value={text} onChange={(event) => setText(event.target.value)} className="flex-1" />
        <EmojiPickerPopover onSelect={({ emoji }) => setText((current) => current + emoji)} />
      </div>
      <EmojiPicker columns={9} onSelect={({ emoji, name }) => console.log(emoji, name)} />
    </div>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `columns?` | `number` | `8` | Cells per row. The picker is as wide as this many cells, and drops columns when its container is narrower. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Cells of 32 / 36 / 44px with 20 / 24 / 28px emoji, and the search field (32 / 40 / 48px), tabs and footer to match. On touch screens every cell is at least 44px. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape of the cells, tabs, search field and skin tone button; scales with the size like the button's. |
| `variant?` | `"surface" \| "plain"` | `"surface"` | surface is a raised panel of its own; plain has no fill, for a popover, a sheet or a card that already has one. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Color of the selected emoji's tint and ring and of the active category tab. |
| `color?` | `string` | `""` | Any CSS color in place of the tone. Leave empty to use the tone. |
| `height?` | `number` | `296` | Height of the scrolling grid in px. Ignored with fullHeight. |
| `fullWidth?` | `boolean` | `false` | Stretches to the container and fits as many columns as there is room for. |
| `fullHeight?` | `boolean` | `false` | The grid takes the height left in the container instead of height. Give the picker or its parent a height. |
| `value?` | `string` |  | The picked emoji, shown with a tint and ring in the grid. Pair it with onValueChange; leave it out to let the picker keep its own. |
| `defaultValue?` | `string` | `""` | The emoji shown as picked at first while uncontrolled. |
| `onValueChange?` | `(emoji: string) => void` |  | Called with the emoji (skin tone applied) on every pick. |
| `onSelect?` | `(emoji: EmojiPickerEmoji) => void` |  | Called on every pick with { emoji, base, name, keywords, category, skinTones }: the emoji with the skin tone, the one without, and its data. |
| `skinTone?` | `"default" \| "light" \| "medium-light" \| "medium" \| "medium-dark" \| "dark"` |  | Controlled skin tone; pair it with onSkinToneChange. |
| `defaultSkinTone?` | `"default" \| "light" \| "medium-light" \| "medium" \| "medium-dark" \| "dark"` | `"default"` | The skin tone until the person picks one. A saved choice (see storageKey) wins after the first render. |
| `onSkinToneChange?` | `(tone: EmojiSkinTone) => void` |  | Called when a skin tone is picked from the header row. |
| `categories?` | `EmojiCategoryId[]` | `"[\"smileys\", \"people\", \"nature\", \"food\", \"activities\", \"travel\", \"objects\", \"symbols\", \"flags\"]"` | Which categories show, in this order: "smileys", "people", "nature", "food", "activities", "travel", "objects", "symbols", "flags". Search only looks in these. |
| `showRecent?` | `boolean` | `true` | A recently used section (and tab) at the top, once something has been picked. |
| `maxRecent?` | `number` | `24` | How many recent emoji are kept, newest first. |
| `recent?` | `string[]` |  | Controlled recent emoji (plain, without skin tone). Uncontrolled they are saved in localStorage under storageKey. |
| `defaultRecent?` | `string[]` |  | The recent row before anything is picked or saved, like your app's favorites. The demo seeds 30 so maxRecent shows. |
| `onRecentChange?` | `(recent: string[]) => void` |  | Called with the new recent list after a pick. |
| `showSearch?` | `boolean` | `true` | The search field in the header. |
| `showTabs?` | `boolean` | `true` | The row of category tabs. Each shows its name in a tooltip. |
| `showSkinTones?` | `boolean` | `true` | The hand button at the end of the header that opens the six skin tones. |
| `showPreview?` | `boolean` | `true` | The footer with the hovered or highlighted emoji, its name and its shortcode. |
| `showSectionLabels?` | `boolean` | `true` | A heading above each section in the grid. |
| `searchPlaceholder?` | `string` | `"Search emoji"` | Placeholder of the search field. |
| `autoFocus?` | `boolean` | `false` | Focuses the search field on mount. EmojiPickerPopover focuses it on open by itself (not on touch screens). |
| `storageKey?` | `string` | `"emoji-picker"` | localStorage prefix for the recent emoji and the skin tone ("<key>:recent", "<key>:skin-tone"). An empty string keeps them in memory only. |
| `emojiFont?` | `string` | `"\"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\", sans-serif"` | The font stack the emoji are drawn with. The emoji are text, so no images load. |
| `icons?` | `Partial<Record<"recent" \| EmojiCategoryId, ReactNode>>` |  | Replaces the icons of the category tabs. |
| `labels?` | `Partial<EmojiPickerLabels>` |  | Every string: search, clear, grid, categories, recent, results, resultCount ({count} results), noResults, noResultsHint, previewEmpty (Pick one of {count} emoji), skinTone, the nine category names and the six tone names. Defaults in EMOJI_PICKER_LABELS. |
| `locale?` | `string` | `""` | Locale for lowercasing the query and formatting numbers: the emoji count in the empty preview (1,870 or 1.870) and the announced result count. Empty uses the UIProvider locale, then the browser's. The demo defaults to en-US. |
| `classNames?` | `Partial<Record<"header" \| "search" \| "input" \| "clear" \| "skinTone" \| "skinToneOption" \| "tabs" \| "tab" \| "indicator" \| "viewport" \| "sectionLabel" \| "row" \| "cell" \| "empty" \| "footer" \| "preview", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the root, merged last. The root takes div props. |

## Parts

### EmojiPickerPopover (component)

A trigger plus the picker in a popover that becomes a bottom sheet below 768px, where the picker fills the sheet. Takes every EmojiPicker prop on top of its own.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `trigger?` | `ReactNode` |  | The element that opens it, rendered asChild. Defaults to a ghost icon button with a smile. |
| `triggerLabel?` | `string` | `"Add emoji"` | aria-label of the default trigger and the sheet's hidden title. |
| `open?` | `boolean` |  | Controlled open state; pair it with onOpenChange. |
| `defaultOpen?` | `boolean` | `false` | Starting state while uncontrolled. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when it opens or closes. |
| `closeOnSelect?` | `boolean` | `true` | Closes after a pick; false keeps it open for several. |
| `side?` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | Preferred side of the trigger; flips when there is no room. |
| `align?` | `"start" \| "center" \| "end"` | `"start"` | Alignment against the trigger. |
| `sideOffset?` | `number` | `8` | Gap to the trigger in px. |
| `responsive?` | `boolean` | `true` | A bottom sheet below 768px. |
| `shortcut?` | `string` |  | A key combo in mod+k notation that toggles it from anywhere. |
| `contentClassName?` | `string` |  | Classes for the floating panel or the sheet. |

### searchEmojis (function)

The picker's search on its own: searchEmojis(query, { categories, skinTone, locale, limit }) returns EmojiPickerEmoji[], names before keywords, best match first. Handy for a :shortcode autocomplete.

### applySkinTone (function)

Returns the emoji with a skin tone, e.g. applySkinTone("👍", "medium") gives 👍🏽. Emoji without tones come back unchanged.

### EMOJI_ROWS (constant)

The bundled data (emoji-picker-data.ts): per category, rows of [emoji, name, keywords joined by |, skin tone positions], from Unicode Emoji 15.0 and the CLDR English annotations.

### EMOJI_CATEGORIES (constant)

The nine category ids in their default order.

### EMOJI_SKIN_TONES (constant)

The six skin tone ids, default first.

### EMOJI_PICKER_LABELS (constant)

The English defaults of labels; spread it to translate a few strings.

### EmojiPickerEmoji (type)

What onSelect receives: { emoji, base, name, keywords, category, skinTones }.

### EmojiSkinTone (type)

"default" | "light" | "medium-light" | "medium" | "medium-dark" | "dark".

### EmojiCategoryId (type)

One of the nine category ids.

## Examples

- **In a message composer**: EmojiPickerPopover next to the field. closeOnSelect off, so several picks go in a row. A bottom sheet on phones.
- **Reactions**: A custom trigger after the reaction chips; a pick adds or toggles one. The info tone marks the selected emoji.
- **Sizes**: sm with 32px cells and lg with 44px cells. On touch screens every size uses 44px.
- **Brand tone and skin tone**: tone="brand", round cells and a medium skin tone applied to every hand and person.
- **Compact**: Three categories, no tabs, skin tones or preview: search and the grid only.
- **Full width on a phone**: fullWidth fits as many 44px columns as the 360px card allows.
- **Shortcode search**: searchEmojis on its own powers a :shortcode autocomplete without the grid.

## See also

- [Emoji Reaction](https://wingo-ui.com/components/emoji-reaction) ([Markdown](https://wingo-ui.com/components/emoji-reaction.md))
- [Popover](https://wingo-ui.com/components/popover) ([Markdown](https://wingo-ui.com/components/popover.md))
- [Search Input](https://wingo-ui.com/components/search-input) ([Markdown](https://wingo-ui.com/components/search-input.md))
- [Virtual List](https://wingo-ui.com/components/virtual-list) ([Markdown](https://wingo-ui.com/components/virtual-list.md))
- [usePersistedState](https://wingo-ui.com/components/use-persisted-state) ([Markdown](https://wingo-ui.com/components/use-persisted-state.md))

---

Canonical: https://wingo-ui.com/components/emoji-picker
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
