EmojiPicker

A React emoji picker with a searchable grid and category rail. For chat inputs, comment boxes and reactions.

EmojiPicker preview

New in 0.11
✨

Install EmojiPickerPanel

1. New project? One command installs the package and wires everything - see Installation:

bash
pnpm dlx zyncat-ui init

2. Import the component - it loads its own stylesheet:

tsx
import { EmojiPickerPanel } from '@zyncat/ui/emoji-picker';

Usage

tsx
import { EmojiPickerPanel, loadEmojiData, getEmojiUrl } from '@zyncat/ui/emoji-picker';
 
<EmojiPickerPanel open={open} onOpenChange={setOpen} onSelect={handleSelect} getEmojiUrl={getEmojiUrl} search trigger={<Button>Add reaction</Button>} />

Compose EmojiPicker in your application. No Tailwind or external styling library is required; every value resolves from Zyncat UI's token vocabulary.

EmojiPicker props

EmojiPicker

PropTypeDefaultDescription
open*boolean—Controlled open state - the panel has no uncontrolled mode.
onOpenChange*(open: boolean) => void—Fires whenever the panel asks to open or close: trigger press, Esc, outside press, sheet drag.
onSelect*(shortcode: string, hexId: string) => void—Fires on pick, with the primary shortcode (`smile`, no colons) and the emoji's hex id.
getEmojiUrl*GetEmojiUrl—Builds the image URL for one emoji at one call site. Pass the bundled `getEmojiUrl` to use Twemoji and Noto.
triggerReactElement | null—Element that both opens the panel and anchors it, when there is no anchor.
offsetnumber—Gap in pixels between a custom `popoverProps.anchor` and the panel. Ignored when the panel anchors to its trigger.
searchboolean—Render the panel's own search field. Always on in sheet mode, where the sheet traps focus and an outside query can no longer reach the panel.
querystring—Drive the results from outside — a `:` chip in a document, your own input.
activateOnActivateOn'click'Whether the trigger, an emoji and a category fire on `pointerdown` (snappier) or wait for `click`.
breakpointstring—Viewport at which the panel becomes a bottom sheet.
popoverPropsOmit<PopoverProps, Forwarded>—Desktop placement — `anchor`, `side`, `align`, `arrow`, and every other Popover knob.
sheetPropsOmit<SheetProps, Forwarded | 'side'>—Narrow-viewport docking — `container`, `dismissible`, and every other Sheet knob.
classNamestring—Extra class(es) merged onto the panel frame.
refRef<EmojiPickerHandle>—Imperative handle - drive the grid from your own field: `handleKey`, `selectFocused`, `renderAll`, `renderFiltered`.

EmojiPickerHandle

PropTypeDefaultDescription
renderAll*() => void—Redraw the full category grid, dropping whatever query was showing.
renderFiltered*(next: string) => void—Redraw the grid as the ranked results for `next`, with the first hit already marked.
handleKey*(event: KeyboardEvent) => boolean—Feed a key event from your own field into the grid - arrows move, Enter picks. Returns true when the grid consumed the key, so you can leave the rest to your input.
selectFocused*() => void—Commit the tile the marker is on, exactly as a click on it would.

Frequently asked questions

Import it from its own subpath - import { EmojiPickerPanel, loadEmojiData, getEmojiUrl } from '@zyncat/ui/emoji-picker' - call loadEmojiData('/emojis.json') once before the panel first opens, then render <EmojiPickerPanel open={open} onOpenChange={setOpen} onSelect={(shortcode, hexId) => addReaction(shortcode, hexId)} getEmojiUrl={getEmojiUrl} search trigger={<Button>Add reaction</Button>} />. It has no uncontrolled mode, so open and onOpenChange are both required.

No - the picker ships with no emoji data inside it. Call loadEmojiData(url | EmojiData) once with your own JSON, or an already-parsed object, before the panel first opens, or it throws 'Emoji data not found'. The docs site loads a 580KB set of 1,923 emoji this way from a static /emojis.json, so the dataset's caching and CDN are yours to control, separately from the component itself.

Images, through the getEmojiUrl(hexId, source) prop you supply. The bundled getEmojiUrl renders Twemoji - inline SVG, or a 72×72 PNG in the grid - and Google's animated Noto emoji for category icons, so every emoji looks identical across operating systems instead of falling back to whichever emoji font the browser has; if an image 404s, that one tile swaps to the native glyph automatically.

Yes. Pass query instead of turning on the panel's own search field, point popoverProps.anchor at your caret's rect, and forward keydown events through the ref's handleKey - selectFocused commits whichever tile the roving marker is on. That is how a chat composer or a comment box wires its own :shortcode trigger without handing the text field over to the picker.

Yes. Arrow keys move a roving marker across the grid and wrap between category sections, Enter commits the focused emoji, and the grid is a role="listbox" of role="option" tiles with aria-activedescendant kept on the field that holds focus, so a screen reader announces each emoji's name as you move. Under prefers-reduced-motion the highlight snaps to its new tile instead of animating between them.

No. @zyncat/ui/emoji-picker ships compiled ESM with its 'use client' directive intact, so it drops into the Next.js App Router with no transpilePackages config; React 19 is its only peer dependency and the npm package has zero runtime dependencies. It composes the library's own Popover and Sheet underneath - no Tailwind and nothing extra to install for either.

Set in Geist & Newsreader — animated by the house engineZyncat UI · Rev 0.11 · MIT · Built by Tabsir Ahammed · Source on GitHub