npx shadcn@latest add https://sdk-components.thesqd.com/r/emoji-picker.jsonimport { EmojiPicker, EmojiPickerPopover } from "@/components/ui/emoji-picker";import { EmojiPicker } from "@/components/ui/emoji-picker";
export function Example() {
return <EmojiPicker onPick={({ emoji, label }) => console.log(emoji, label)} />;
}npx shadcn@latest add https://sdk-components.thesqd.com/r/emo-suggester.jsonEmojiPicker (frimousse Root — data, virtualised grid, keyboard nav)
├── Input search (the visible query field)
├── Recent row (up to `columns` recent picks — Popular until there are any)
├── Viewport (nothing typed; scroll-fade)
│ ├── Loading → Spinner
│ └── List (sticky category headers, a ghost Button per emoji)
├── Search results (something typed; ONE grid, scroll-fade, arrows + Enter)
│ ├── the `suggester`'s answers for the query read as a phrase, first
│ ├── then name and keyword matches, each emoji once
│ └── "No emoji match" only when both came back empty
└── Footer
├── Active emoji + label (hovered or keyboard-selected)
└── Skin tone Button (cycles the five tones and none)
EmojiPickerPopover (Popover + EmojiPicker variant="plain", closes on pick)
└── trigger={<Button … />}
Recent emoji and the skin tone are uncontrolled unless you pass
`recent` / `onRecentChange` and `skinTone` / `onSkinToneChange` —
store them in a user's settings to make them follow the person.
`addRecentEmoji(recent, emoji)` is exported for keeping your own list.
createEmoSuggester(options) (emo-suggester.ts, a separate install)
└── → EmojiSuggester { suggest(query, { skinTone, limit }) }EmojiPicker
| Prop | Type | Default | Description |
|---|---|---|---|
onPick | (emoji: { emoji: string; label: string }) => void | — | Called with the picked emoji, skin tone applied. |
variant | "card" | "plain" | "card" | `card` draws its own card surface; `plain` draws none (inside a popover or panel). |
columns | number | 8 | Cells per row. The panel's width follows it; 8 fits a 375px phone. |
emojibaseUrl | string | frimousse's jsDelivr CDN | Where `{locale}/data.json` and `{locale}/messages.json` are served. Self-host when a CSP blocks the CDN. |
recent | string[] | — | Recent emoji, newest first. Uncontrolled when omitted. |
defaultRecent | string[] | — | Shown as a Popular row while there are no recent picks yet. |
onRecentChange | (recent: string[]) => void | — | Called with the new list after every pick. |
skinTone | "none" | "light" | "medium-light" | "medium" | "medium-dark" | "dark" | — | Uncontrolled (starting at `none`) when omitted. |
onSkinToneChange | (skinTone: SkinTone) => void | — | Called when the footer button cycles the tone. |
suggester | EmojiSuggester | — | Reads the query as a phrase; its answers lead the search results, ahead of name and keyword matches. `createEmoSuggester()` is one; any `{ suggest }` works. |
searchPlaceholder | string | "Search emoji" | Placeholder and accessible name of the search field. |
autoFocus | boolean | — | Focus the search field on mount. |
className | string | — | Class names merged onto the root. |
EmojiPickerPopover
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | ReactElement | — | The element that opens the picker — usually a ghost icon Button. |
align | "start" | "center" | "end" | "start" | Alignment against the trigger. |
side | "top" | "right" | "bottom" | "left" | — | Side of the trigger to open on. |
… | EmojiPickerProps | — | Every picker prop except `variant` and `autoFocus` (always `plain`, always focused). |
createEmoSuggester(options)
| Prop | Type | Default | Description |
|---|---|---|---|
modelBaseUrl | string | Hugging Face | Base URL serving Emo's model files yourself. |
litertWasmDir | string | jsDelivr | URL of the LiteRT.js `wasm/` directory. |
accelerator | "wasm" | "webgpu" | "webnn" | "wasm" | LiteRT.js backend. |
minConfidence | number | 0.1 | Answers below this are dropped; the model answers even for gibberish. |