Emoji Picker React component for Tailwind CSS v4
v1.0.0Added in 1.14.0
A React emoji picker with search, category tabs, recent emoji, skin tones and a preview, bundling 1,870 emoji with names and keywords.
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.
Emoji Picker is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add emoji-picker, together with Button, Popover, Tooltip, useControllableState, useLocale and 5 more and the npm packages @tanstack/react-virtual, lucide-react and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide ยท Next.js setup ยท Vite setup
npx wingo-ui@latest add emoji-pickerEmoji Picker is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import EmojiPicker from @/components/ui/emoji-picker.
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
columnsdefault8numberCells per row. The picker is as wide as this many cells, and drops columns when its container is narrower.
sizedefault"md""sm" | "md" | "lg"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.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner shape of the cells, tabs, search field and skin tone button; scales with the size like the button's.
variantdefault"surface""surface" | "plain"surface is a raised panel of its own; plain has no fill, for a popover, a sheet or a card that already has one.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Color of the selected emoji's tint and ring and of the active category tab.
colordefault""stringAny CSS color in place of the tone. Leave empty to use the tone.
heightdefault296numberHeight of the scrolling grid in px. Ignored with fullHeight.
fullWidthdefaultfalsebooleanStretches to the container and fits as many columns as there is room for.
fullHeightdefaultfalsebooleanThe grid takes the height left in the container instead of height. Give the picker or its parent a height.
valuestringThe 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.
defaultValuedefault""stringThe emoji shown as picked at first while uncontrolled.
onValueChange(emoji: string) => voidCalled with the emoji (skin tone applied) on every pick.
onSelect(emoji: EmojiPickerEmoji) => voidCalled 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.
defaultSkinTonedefault"default""default" | "light" | "medium-light" | "medium" | "medium-dark" | "dark"The skin tone until the person picks one. A saved choice (see storageKey) wins after the first render.
onSkinToneChange(tone: EmojiSkinTone) => voidCalled when a skin tone is picked from the header row.
categoriesdefault"["smileys", "people", "nature", "food", "activities", "travel", "objects", "symbols", "flags"]"EmojiCategoryId[]Which categories show, in this order: "smileys", "people", "nature", "food", "activities", "travel", "objects", "symbols", "flags". Search only looks in these.
showRecentdefaulttruebooleanA recently used section (and tab) at the top, once something has been picked.
maxRecentdefault24numberHow many recent emoji are kept, newest first.
recentstring[]Controlled recent emoji (plain, without skin tone). Uncontrolled they are saved in localStorage under storageKey.
defaultRecentstring[]The recent row before anything is picked or saved, like your app's favorites. The demo seeds 30 so maxRecent shows.
onRecentChange(recent: string[]) => voidCalled with the new recent list after a pick.
showSearchdefaulttruebooleanThe search field in the header.
showTabsdefaulttruebooleanThe row of category tabs. Each shows its name in a tooltip.
showSkinTonesdefaulttruebooleanThe hand button at the end of the header that opens the six skin tones.
showPreviewdefaulttruebooleanThe footer with the hovered or highlighted emoji, its name and its shortcode.
showSectionLabelsdefaulttruebooleanA heading above each section in the grid.
searchPlaceholderdefault"Search emoji"stringPlaceholder of the search field.
autoFocusdefaultfalsebooleanFocuses the search field on mount. EmojiPickerPopover focuses it on open by itself (not on touch screens).
storageKeydefault"emoji-picker"stringlocalStorage prefix for the recent emoji and the skin tone ("<key>:recent", "<key>:skin-tone"). An empty string keeps them in memory only.
emojiFontdefault""Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif"stringThe font stack the emoji are drawn with. The emoji are text, so no images load.
iconsPartial<Record<"recent" | EmojiCategoryId, ReactNode>>Replaces the icons of the category tabs.
labelsPartial<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.
localedefault""stringLocale 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.
classNamesPartial<Record<"header" | "search" | "input" | "clear" | "skinTone" | "skinToneOption" | "tabs" | "tab" | "indicator" | "viewport" | "sectionLabel" | "row" | "cell" | "empty" | "footer" | "preview", string>>Extra classes for the inner parts.
classNamestringExtra classes for the root, merged last. The root takes div props.
Parts
EmojiPickerPopover
componentA 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.
triggerReactNodeThe element that opens it, rendered asChild. Defaults to a ghost icon button with a smile.
triggerLabeldefault"Add emoji"stringaria-label of the default trigger and the sheet's hidden title.
openbooleanControlled open state; pair it with onOpenChange.
defaultOpendefaultfalsebooleanStarting state while uncontrolled.
onOpenChange(open: boolean) => voidCalled when it opens or closes.
closeOnSelectdefaulttruebooleanCloses after a pick; false keeps it open for several.
sidedefault"bottom""top" | "right" | "bottom" | "left"Preferred side of the trigger; flips when there is no room.
aligndefault"start""start" | "center" | "end"Alignment against the trigger.
sideOffsetdefault8numberGap to the trigger in px.
responsivedefaulttruebooleanA bottom sheet below 768px.
shortcutstringA key combo in mod+k notation that toggles it from anywhere.
contentClassNamestringClasses for the floating panel or the sheet.
searchEmojis
functionThe 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
functionReturns the emoji with a skin tone, e.g. applySkinTone("๐", "medium") gives ๐๐ฝ. Emoji without tones come back unchanged.
EMOJI_ROWS
constantThe 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
constantThe nine category ids in their default order.
EMOJI_SKIN_TONES
constantThe six skin tone ids, default first.
EMOJI_PICKER_LABELS
constantThe English defaults of labels; spread it to translate a few strings.
EmojiPickerEmoji
typeWhat onSelect receives: { emoji, base, name, keywords, category, skinTones }.
EmojiSkinTone
type"default" | "light" | "medium-light" | "medium" | "medium-dark" | "dark".
EmojiCategoryId
typeOne 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.
Dependencies
Files
- components/ui/emoji-picker.tsx
- components/ui/emoji-picker-data.ts
FAQ
Is the React Emoji Picker component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Emoji Picker?
Run npx wingo-ui@latest add emoji-picker. It also adds the Wingo UI items it builds on (Button, Popover, Tooltip, useControllableState, useLocale and 5 more) and the npm packages @tanstack/react-virtual, lucide-react and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/emoji-picker.
How does Emoji Picker behave on phones?
On a touch screen every cell is 44px, and EmojiPickerPopover opens as a bottom sheet the picker fills below 768px.
See also
More React input components
Articles using this component
- React Emoji Picker: Search, Skin Tones and Phone Sheets
Component guides9 min read