Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
  1. Home
  2. Blog
  3. Component guides
  4. React Emoji Picker: Search, Skin Tones and Phone Sheets
Auf Deutsch lesen
  1. Blog
  2. Component guides
  3. React Emoji Picker: Search, Skin Tones and Phone Sheets
Component guides
Component guides

React Emoji Picker: Search, Skin Tones and Phone Sheets

SR
Serban Rusu ยท Founder of Wingo UI
Nov 6, 2026 ยท 9 min read

On this page

0%
  1. What should a React emoji picker include?
  2. Should you bundle the emoji data or fetch it?
  3. How do you add the emoji picker to a React app?
  4. How do you insert an emoji at the cursor in a message composer?
  5. How does an emoji picker behave on a phone?
  6. How do emoji picker skin tones work?
  7. Is there a React emoji picker for shadcn?
  8. Should message reactions use the full picker?
  9. What are the common emoji picker pitfalls?
  10. Next step
  11. FAQ
    1. What is the best React emoji picker library?
    2. Does shadcn/ui have an emoji picker?
    3. How do skin tones work in an emoji picker?
    4. Why do flag emoji show as letters on Windows?
    5. Is the Wingo UI Emoji Picker free?

TL;DR

A good React emoji picker has ranked keyword search, category tabs, a recent row, skin tones that stick, a name preview, a virtualized grid, full keyboard support and a bottom sheet on phones. Bundling the data costs about 31 KB gzipped for the 1,870 emoji in Wingo UI's picker and works offline, while fetching it from a CDN keeps it out of your bundle but adds a request on the first open. Either way, lazy-load the picker so pages that never open it pay nothing.

Published Nov 6, 2026

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.

Library (as of November 6, 2026)Where the data comes fromStyling
Wingo UI Emoji PickerBundled in a source file you own, parsed on first useDesign tokens, light and dark
Frimousse (opens in a new tab) 0.4.0Fetched from Emojibase on jsDelivr by default, cached locally; emojibaseUrl points it at your own copyUnstyled parts
emoji-mart (opens in a new tab) (@emoji-mart/react 1.1.1, last published January 2023)Your choice: import @emoji-mart/data or fetch it remotelyStyled, themeable
emoji-picker-react (opens in a new tab) 5.1.4Ships in the package; native emoji by default, image styles load from jsDelivrStyled, themeable, plus headless primitives

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.

tsx
"use client"
import { useState } from "react"
import dynamic from "next/dynamic"
import { SmilePlus } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Popover } from "@/components/ui/popover"
// the picker and its data load on the first open, not with the page
const EmojiPicker = dynamic(() => import("@/components/ui/emoji-picker"), {
ssr: false,
loading: () => <p className="p-4 text-sm text-muted-foreground">Loading emojiโ€ฆ</p>,
})
export function LazyEmojiButton({ onPick }: { onPick: (emoji: string) => void }) {
const [open, setOpen] = useState(false)
return (
<Popover
open={open}
onOpenChange={setOpen}
size="auto"
padding="none"
sheetTitle="Add emoji"
trigger={
<Button variant="ghost" size="icon" radius="full" aria-label="Add emoji">
<SmilePlus />
</Button>
}
>
<EmojiPicker
variant="plain"
onSelect={({ emoji }) => {
onPick(emoji)
setOpen(false)
}}
/>
</Popover>
)
}

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):

bash
npx wingo-ui@latest add emoji-picker

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.

tsx
import { EmojiPicker } from "@/components/ui/emoji-picker"
export function StatusEmoji({ onChange }: { onChange: (emoji: string) => void }) {
return (
<EmojiPicker
columns={9}
size="md"
height={296}
maxRecent={24}
onSelect={({ emoji }) => onChange(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:

Search for an emoji, hover one to read its name, then pick a skin tone and watch every hand change
$ npx wingo-ui@latest add emoji-picker
ProEmoji Picker docs

Every 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:

tsx
"use client"
import { useRef, useState } from "react"
import { EmojiPickerPopover } from "@/components/ui/emoji-picker"
export function MessageComposer() {
const [text, setText] = useState("")
const fieldRef = useRef<HTMLTextAreaElement>(null)
const caret = useRef<{ start: number; end: number } | null>(null)
const remember = () => {
const field = fieldRef.current
if (field) caret.current = { start: field.selectionStart, end: field.selectionEnd }
}
const insert = (emoji: string) => {
const { start, end } = caret.current ?? { start: text.length, end: text.length }
setText(text.slice(0, start) + emoji + text.slice(end))
caret.current = { start: start + emoji.length, end: start + emoji.length }
}
return (
<div className="flex items-end gap-1 rounded-3xl bg-foreground/5 p-1 ps-4">
<textarea
ref={fieldRef}
value={text}
rows={1}
aria-label="Message"
placeholder="Message Emma Carter"
onChange={(event) => setText(event.target.value)}
onSelect={remember}
onBlur={remember}
className="min-h-9 flex-1 resize-none bg-transparent py-2 text-base outline-hidden md:text-sm"
/>
<EmojiPickerPopover side="top" align="end" closeOnSelect={false} onSelect={({ emoji }) => insert(emoji)} />
</div>
)
}

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:

Press the button and drag onto an emoji, or tap it open and pick one
$ npx wingo-ui@latest add emoji-reaction
FreeEmoji Reaction docs

The 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 emojiFont to a stack that starts with a webfont that has flags if your users need them, or hide the category with categories.
  • Counting characters. "๐Ÿ‘๐Ÿฝ".length is 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 control recent yourself.
  • 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.

Components in this post

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

    Pro
  • Emoji Reaction

    A React emoji reaction button in the chat tapback style: it opens a bar of Apple emoji and sends copies of your pick floating up.

    Free
  • Popover

    A React popover for rich content anchored to a trigger, like a quick filter, form or share panel, that becomes a bottom sheet on phones.

    Free
  • Comment Thread

    A React comment thread for records: replies, reactions, edit and delete, @mentions and a composer that sends optimistically with retry.

    Pro

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.

Try it in your project

Emoji Picker

Pro

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

75 components free, no account needed. Pro from $8 a month.

Open Emoji Picker liveSee pricing
  • Components
  • React
  • Emoji
  • Chat
  • Mobile

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Component guides

React Mentions: @People and #Tags That Survive Renames

React mentions done right: @people and #tags stored as id-based markup that survives renames, suggestions at the caret, and a list that works on phones.

SRSerban RusuยทOct 19, 2026ยท9 min read
Component guides

React Color Picker: HEX, RGB, HSL, Eyedropper and OKLCH

Build a React color picker with HEX, RGB and HSL inputs, swatches, opacity and the EyeDropper API, then feed its value into OKLCH design tokens in Tailwind v4.

SRSerban RusuยทOct 16, 2026ยท9 min read
Mobile UI in React

Mobile-First React Components: 9 Rules With Code

Mobile-first React components, rule by rule: bottom sheets instead of popovers, 44px touch targets, safe areas, dvh, keyboard-aware bars and tables as cards.

SRSerban RusuยทOct 9, 2026ยท21 min read
Newsletter

Get new posts by email

New posts, product news and occasional offers from Wingo UI. We count clicks to keep it relevant.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • For teams
  • Theme generator

Resources

  • Docs
  • Contact
  • Programs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Compare

  • Wingo UI vs paid kits
  • Wingo UI vs shadcn/ui
  • shadcn/ui alternatives
  • MUI alternatives
  • Tailwind Plus alternatives
  • Magic UI alternatives
  • Free components

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

ยฉ 2026 Wingo UI

Wingo UI