React Emoji Picker: Search, Skin Tones and Phone Sheets
A React emoji picker looks like a grid of smileys until you build one. People type "party" and expect ๐ first, they want the emoji they always use at the top, they want their skin tone applied to every hand without picking it again, and they open it from a message field on a phone where the keyboard already takes half the screen. Then someone asks why the chat page got 30 KB heavier. This guide covers each of those with the Emoji Picker from Wingo UI. We build that library, so we are not neutral; outside facts link to their sources and are dated.
What should a React emoji picker include?
Eight things: ranked search, category tabs, a recent row, skin tones, a preview, a virtualized grid, keyboard support and a phone layout. Check any react emoji picker component against this list:
- Keyword search with ranking. A match on a word of the name ("party popper") beats a match on a keyword, which beats a match inside a word. Among equal matches, the popular emoji go first, so "heart" gives โค๏ธ before ๐.
- Category tabs that jump to a section and light up as you scroll past it.
- A recent row at the top, saved between visits.
- Skin tones, picked once and applied everywhere.
- A preview with the emoji large, its name and a shortcode such as
:party_popper:, because many emoji look alike at 24px. - A virtualized grid. 1,870 buttons is a lot to mount every time a popover opens; render only the rows on screen.
- Keyboard support: arrows, Home, End and Page Up / Page Down in the grid, Enter to pick, Escape to clear the query, and typing a letter goes back to search.
- A phone layout: 44px cells and a bottom sheet instead of a floating panel.
The Wingo UI picker covers all eight. Its grid uses @tanstack/react-virtual, the cells grow to 44px on a coarse pointer, and the result count is announced in a live region for screen readers.
Should you bundle the emoji data or fetch it?
Bundle it when the picker must open instantly and work offline; fetch it when you want the bytes out of your JavaScript. Every react emoji selector makes this choice, and the libraries make it differently. We measured our own data file on November 6, 2026: emoji-picker-data.ts holds 1,870 emoji with their CLDR English names and keywords from Unicode Emoji 15.0, at 108 KB of source, 31 KB with gzip and 26 KB with Brotli. The emoji are text drawn by the system font, so no images load at all.
Bundling means the picker opens at once, works offline and never depends on a third-party CDN being up. Fetching keeps the bytes out of your JavaScript and lets you update emoji without shipping code, at the price of a request (and a spinner) on the first open. The emoji-mart README lists the same trade-off for its two modes. If you want an emoji-picker-react alternative because of data or styling, this table is where the choice happens.
What matters more than which side you pick: do not load any of it on pages where nobody opens the picker. Our picker parses the rows only on first use, but the file is still in the chunk that imports it, so lazy-load the component.
The Popover mounts its content only when it opens, so next/dynamic starts the import on the first tap. The cost is a short loading state once per visit, and on a phone this version shows the picker at its set height inside the sheet. If you would rather have it instant and filling the sheet, import EmojiPickerPopover directly and accept the 31 KB on that route.
How do you add the emoji picker to a React app?
Install it with the CLI. It copies emoji-picker.tsx and the data file into components/ui/, plus what they import (Button, Popover, Tooltip and a few hooks). It is a Pro item, so the CLI needs a Pro account (npx wingo-ui@latest login):
EmojiPicker renders inline; EmojiPickerPopover adds a trigger and opens the picker in a popover. Both report a pick through onSelect, which receives { emoji, base, name, keywords, category, skinTones }: the emoji with the skin tone applied, the one without, and its data. If you only need the string, onValueChange gets just the emoji.
Search for "pizza" or "rocket", hover an emoji to see it in the preview footer, then open the hand button and pick a tone:
$ npx wingo-ui@latest add emoji-pickerEvery visual part is a prop: size (32, 36 or 44px cells), radius, tone for the selected tint, variant="plain" when it sits in a card that already has a fill, and showTabs, showPreview, showSkinTones and categories to trim it down. The recent emoji and the skin tone are saved in localStorage under storageKey (default emoji-picker); pass an empty string to keep them in memory, or control them with recent and skinTone if you store preferences on the server.
How do you insert an emoji at the cursor in a message composer?
Save the field's selection while it has focus, then splice the emoji into the text at that position. Quick usage examples, ours included, append the emoji to the end of the text, which is wrong as soon as someone moves the caret:
closeOnSelect={false} keeps the picker open for several picks in a row, which is how people use it in chat, and the caret ref moves forward so each pick lands after the last one. Note emoji.length: ๐๐ฝ is 4 UTF-16 units, so the caret math must use the string length, never 1.
How does an emoji picker behave on a phone?
It should open as a bottom sheet with 44px cells and leave the keyboard closed until you ask for it. Three things go wrong on phones, and each has a fix.
A floating panel does not fit. A 300px popover anchored to a 44px button covers the thumb that opened it and fights the keyboard for space. Below 768px, EmojiPickerPopover opens a bottom sheet you can drag down, and the picker fills it at min(64dvh, 520px) with as many 44px columns as the width allows. The sheet pads its bottom with env(safe-area-inset-bottom) so the last row clears the home indicator. We wrote about the sheet itself in React bottom sheet for the web.
The keyboard pops up when you did not ask for it. On desktop the popover focuses the search field so you can type at once. On a touch screen it focuses the panel instead, so opening the picker shows the grid without the keyboard; tapping the search field brings it up. Below 768px the search input uses 16px text, so iOS does not zoom the page, and inputMode="search" gives the keyboard a search key.
Taps land on the wrong cell. At 36px a thumb hits the neighbor. On a coarse pointer every cell grows to at least 44px, whatever size says, and cells carry touch-manipulation so a double tap does not zoom.
One more: input methods for languages like Japanese compose text before committing it. The search ignores Enter while isComposing is true, so confirming a word does not pick the first emoji.
How do emoji picker skin tones work?
Unicode defines five skin tone modifiers, U+1F3FB to U+1F3FF. A modifier goes right after the code point that takes it: ๐ plus U+1F3FD gives ๐๐ฝ. Emoji with two people, such as people holding hands, take a modifier after each person, and most emoji take none.
The data file stores, for each emoji, where its modifier goes, so the picker applies one choice to every hand and person and leaves ๐ alone. The choice is saved under <storageKey>:skin-tone. Outside the picker, applySkinTone("๐", "medium") returns ๐๐ฝ, and searchEmojis(query, { limit }) gives you the same ranked search for a :shortcode autocomplete in a plain text field.
Is there a React emoji picker for shadcn?
Not in shadcn/ui itself: as of November 6, 2026 its components page (opens in a new tab) lists Popover but no emoji picker. A react emoji picker shadcn projects can add comes from a third-party registry instead: Frimousse and emoji-picker-react 5 both publish a shadcn registry item in their READMEs, and Wingo UI's picker installs the same way (source files in components/ui, cn(), design tokens) through its own CLI. If you also use @mentions in the same composer, React mentions covers that field.
Should message reactions use the full picker?
No. Reacting to a message needs a handful of choices, not 1,870. The free Emoji Reaction button opens a short bar of five Apple emoji (images from a CDN by default, set with emojiBaseUrl) and sends copies of the pick floating up; press and drag straight onto an emoji to pick it in one gesture:
$ npx wingo-ui@latest add emoji-reactionThe Comment Thread takes the same approach: its reactions prop sets the row of quick reactions under each comment (๐ โค๏ธ ๐ ๐ ๐ฎ ๐ by default). Keep the full picker for the composer, or behind a "more" button next to the reaction chips.
What are the common emoji picker pitfalls?
Flags, string length, hydration and old fonts cause most of the bug reports:
- Flags on Windows. Segoe UI Emoji has no national flag glyphs, so ๐บ๐ธ renders as "US", as a moderator answer on Microsoft Q&A (opens in a new tab) confirms (checked November 6, 2026). Set
emojiFontto a stack that starts with a webfont that has flags if your users need them, or hide the category withcategories. - Counting characters.
"๐๐ฝ".lengthis 4. For a character limit, count graphemes with[...new Intl.Segmenter().segment(text)].length. - Hydration. Recent emoji come from
localStorage, which the server cannot read. The picker renders the server value first and reads storage after hydration, so the markup matches; do the same if you controlrecentyourself. - Old data and old fonts. Our data stops at Emoji 15.0, so emoji from later Unicode versions are not in the grid, and a device whose font is older than that still draws some of these as empty boxes.
For more component guides like this one, see the component guides category and the pillar on React form components.
Next step
Open the Emoji Picker docs to try every prop in the playground, then install it with npx wingo-ui@latest add emoji-picker. It is part of Wingo UI Pro (pricing); the Popover and Emoji Reaction are free with a free account.
FAQ
What is the best React emoji picker library?
It depends on how much you want to own. As of November 6, 2026, emoji-picker-react (5.1.4) is a styled drop-in with headless primitives, Frimousse (0.4.0) is unstyled and fetches its data from a CDN, and Wingo UI's Emoji Picker installs as source code with bundled data, design tokens and a phone sheet.
Does shadcn/ui have an emoji picker?
No. As of November 6, 2026 the shadcn/ui components page lists Popover but no emoji picker. Frimousse and emoji-picker-react publish shadcn registry items, and Wingo UI installs its picker as source through its own CLI.
How do skin tones work in an emoji picker?
A skin tone is one of five Unicode modifier characters (U+1F3FB to U+1F3FF) placed after the code point that takes it, so ๐ becomes ๐๐ฝ. Only some emoji accept one, so a picker should apply the chosen tone to all of those, leave the rest alone and remember the choice.
Why do flag emoji show as letters on Windows?
Segoe UI Emoji, the Windows emoji font, has no national flag glyphs, so a flag renders as its two-letter region code. Point the picker's font stack at a webfont that has flags if your users need them.
Is the Wingo UI Emoji Picker free?
No, the Emoji Picker and Comment Thread are part of Wingo UI Pro ($8 a month, $80 a year or $150 once). The Popover it opens in and the Emoji Reaction button are free.
- Components
- React
- Emoji
- Chat
- Mobile


