# v5 API reference
Choose batteries included or BYOD (bring your own design, design language and design library). Both use the same behavior engine. The v5 public API is designed from the consumer inward:
- `` remains the primary path;
- v4 source compatibility is preserved wherever the architecture does not require a break;
- new APIs are added only for demonstrated needs;
- composition is layered: rearrange the macro structure, read picker state through hooks, and — when a brand needs it — replace the markup of emoji cells and category headers while the library keeps owning their behavior.
## 1. Batteries included
```tsx
import EmojiPicker from 'emoji-picker-react';
function Composer() {
return ;
}
```
Consumers do not need primitives to upgrade to v5.
## 2. Additions to the default picker
v5 adds:
```ts
type EmojiPickerV5Additions = {
searchValue?: string;
defaultSearchValue?: string;
onSearchChange?: (value: string) => void;
searchLabel?: string;
suggestedEmojis?: string[]; // §7
keepSearchFocus?: boolean; // §4a
variationsDisabled?: boolean; // §10b
onReactionsModeChange?: (reactionsOpen: boolean) => void;
labels?: Partial; // §5a
skinTone?: SkinTonesValue; // §5b, controlled
unstyled?: boolean; // §11
columns?: number; // §11a
components?: PickerComponents; // shared managed-control replacements
emojiData?: EmojiData | EmojiDataLoader; // §12a, loader form is new
};
```
`skinTonePickerLocation` also accepts `SkinTonePickerLocation.NONE` (§8).
The assembled picker retains its legacy visibility and placement props; see [PROPS.md](../../PROPS.md). Those switches are not primitive Root props.
## 3. Literal values without removing enums
v5 accepts readable literals:
```tsx
```
Existing enum-based code remains valid:
```tsx
```
Existing enum exports remain part of v5.
## 4. Controlled search
```tsx
function ControlledPicker() {
const [search, setSearch] = useState('');
return (
);
}
```
Uncontrolled:
```tsx
```
The visible value and callback use raw user text. Filtering uses a normalized derived query and is debounced; see [primitive state and actions](./PRIMITIVES.md#state-and-actions).
Type-to-search differs slightly between controlled and uncontrolled usage:
- both modes: focus Search immediately, then behave like ordinary input editing;
- controlled: the keystroke is a proposal like any other edit, so a parent that ignores it simply leaves the value unchanged — the same thing that happens when you type into a controlled input whose parent ignores you;
- focus transfer is never conditional on acceptance.
If Search is omitted, built-in type-to-search behaves like `searchDisabled`, while an external application input may still drive filtering through controlled `searchValue`.
## 4a. Keep focus in search
By default, the arrow key toward the grid moves focus from the search input into it. With `keepSearchFocus`, focus stays in the input and the arrow keys move a highlight instead, so the user can keep typing:
```tsx
```
- The arrow toward the grid highlights the first emoji in view (the first result while searching): ArrowDown when the grid is below the input, ArrowUp when it is above, and Left or Right when it is beside the input (with the caret at that end of the text). The arrow keys then move the highlight through the grid, including rows that are not rendered yet. Otherwise Left and Right edit the text until an emoji is highlighted; from then on they move the highlight, and Escape gives them back to the text. With Shift, Ctrl, Alt or Meta, arrows always stay with the input (selecting text, moving by word), with or without a highlight. Left and Right follow the reading direction in RTL.
- Enter selects the highlighted emoji, or the first result when nothing is highlighted, even if the results are scrolled. Focus stays in the input. While filtering is pending, Enter queues one selection for the matching results after they commit. Editing the query, pressing another key, or leaving the input cancels that queued selection.
- Once started, the highlight follows the results: when the applied search, dataset or `suggestedEmojis` changes, it moves to the first result and updates Preview.
- Moving past the grid edge that faces the input (ArrowUp from the first row when the input is above, Left from the first column when it is on the left), Escape, or leaving the input removes the highlight, and cancels a move that has not landed yet. Escape ends only the highlight, even one whose emoji is scrolled out and unmounted; with nothing highlighted, it clears the search as usual.
- Hovering an emoji previews it without taking focus from the input (true in both modes: hover never takes focus from the search input). The highlight stays where it is, and Enter still selects it; once the pointer leaves the emoji, the preview shows the highlighted one again.
- Space types a space, so it does not open skin tone variations in this mode.
An Escape consumed by the picker stops propagation in window capture, before document-capture and bubbling host dismissal handlers. With nothing left to undo, Escape is left untouched for the host.
The input becomes an ARIA combobox with a grid popup: `aria-controls` names the grid while both are mounted, and `aria-activedescendant` names the highlighted cell (it is empty while virtualization has the highlighted cell scrolled out). That cell carries `data-epr-highlighted` and is the active emoji for Preview, `useActiveEmoji()` and `emoji.isActive`. The default appearance styles it like keyboard focus; with `unstyled` or custom cells, style `[data-epr-highlighted]` as well.
Primitive compositions pass the same prop to `Root`.
The highlight code is a separate chunk that the first picker with `keepSearchFocus` loads when it mounts, so pickers without the prop never load it. Until it arrives, the arrow keys stay in the input; typing and Enter work from the start. If it fails to load (offline, a Content Security Policy, or a deploy that removed it), the arrow keys move focus into the grid as they do without the prop; the next picker mount tries again. Turning `keepSearchFocus` off ends a highlight, and Enter then selects the first result in view, as without the prop.
## 5. Search accessibility label
The search input's accessible label, an English constant in v4, is configurable:
```tsx
```
`searchLabel` defaults to the v4 English label, so plug-and-play behavior is unchanged.
Primitive consumers may also supply a consumer `aria-label` through Search `inputProps`; when supplied there, that explicit primitive-level label wins for that Search instance.
## 5a. Localize every string
`labels` covers every user-facing string the picker renders or announces. Omitted keys keep their English (v4) defaults; `labels` wins over the individual `searchPlaceholder`/`searchLabel`/`searchClearButtonLabel` props, which keep working.
```tsx
```
Category names come from the localized `emojiData` (or `categories`); the preview caption from `previewConfig.defaultCaption`.
## 5b. Controlled skin tone
```tsx
const [tone, setTone] = useState(SkinTones.NEUTRAL);
```
While `skinTone` is present it is the source of truth (`defaultSkinTone` is ignored); choosing a tone reports through `onSkinToneChange`.
## 5c. Native emoji support detection
`emojiStyle` defaults to `native`. Native glyphs come from the installed emoji font, which can lag behind the dataset. The picker probes that font on the client and hides:
- emojis newer than the platform renders (they would show as empty boxes);
- individual country flags where the installed font has no flag glyph;
- individual missing glyphs, broken join/modifier sequences, and subdivision flags that fall back to a plain black flag.
Detection never delays the first paint. Before paint, the picker draws one sample per emoji version and a country flag, and hides versions (and country and subdivision flags) the font cannot draw. Exact checks then run only for identities about to be shown: grid cells within a viewport of the visible rows (in either scroll direction) with their skin tone variants, and the reaction choices. They run in small batches between tasks and are cached, so each glyph is checked at most once per font; the rest of the inventory keeps its version prediction. When a check disagrees with the prediction the picker rerenders; otherwise nothing changes. Pending keyboard or tab navigation is never cancelled by these results. Filtering precedes grid layout, so removed cells leave no gaps; search result counts use the same filtered inventory. Reactions and the variation menu also omit unavailable choices, and the managed Preview does not draw unsupported native glyphs. This applies to both the default picker and primitives. Image styles and image-based custom emojis do not depend on OS glyph support.
`emojiVersion` remains an additional maximum version: it never disables native detection or makes unsupported glyphs available. SSR and hydration-first output stay deterministic; detection is client-only. The standalone `Emoji` component outside a picker Root does not participate in Root inventory filtering.
Detection is heuristic, not a browser-provided glyph-coverage guarantee. It compares rendered width and canvas artwork under two text colors, which distinguishes ordinary missing-glyph/text fallback from fixed-color emoji artwork, including black-and-white artwork. Unusual fonts may be misclassified. Inconclusive sequence probes preserve those entries. If canvas readback is blocked or no stable emoji artwork baseline is available, the picker preserves the whole inventory rather than hiding everything. Avoid promising complete suppression in those environments.
Results are cached per document, font and sequence. Bounded canvas batches share pixel readbacks; later pickers on the same page reuse cached results without probing again. A flag polyfill font can be supplied through `--epr-emoji-font-family`; detection measures that same font.
Detection follows font changes selected by attributes on Root or its ancestors (including `class`, `style`, and `data-theme`), even when those fonts are already loaded. It also re-checks in the background after webfonts finish loading, keeping the current results on screen until the new ones are ready.
## 6. Observe reactions/full-picker mode
```tsx
{
updateLayout(reactionsOpen);
}}
/>
```
This is an observer, not a second controlled mode API.
Existing `reactionsDefaultOpen`, `allowExpandReactions`, `reactions`, `onReactionClick`, and `collapseToReactions()` remain.
## 7. Caller-defined suggestions
```tsx
```
For standard emoji IDs:
- matching is case-insensitive;
- matching ignores where U+FE0F appears: `'2764'` and `'2764-fe0f'` both resolve to the dataset's own spelling, the same rule `reactions` and `hiddenEmojis` use;
- a valid variation remains that exact variation for rendering rather than collapsing to the neutral base emoji;
- duplicates are removed by canonical normalized ID, first occurrence wins.
Custom emoji IDs use the same case-insensitive rule: IDs are lowercased when indexed. Custom display names retain the supplied capitalization; search still matches them case-insensitively.
Entries may be unified IDs, custom emoji IDs, or the emoji characters themselves (`'🧠'`, `'❤️'`), so recents stored as inserted text can be passed through unchanged; unknown entries are ignored.
When `suggestedEmojis` is supplied, it determines the Suggested category contents/order, and the picker records nothing: picks are not written to the browser's recents (`localStorage`), so an app that owns its recents updates them from `onEmojiClick`. `suggestedEmojisMode` remains relevant only when `suggestedEmojis` is absent.
See [primitive state and actions](./PRIMITIVES.md#state-and-actions).
## 8. Structural primitives
```tsx
import * as EmojiPicker from 'emoji-picker-react/primitives';
function ProductPicker() {
return (
);
}
```
Root renders exactly the children supplied by the caller. It never inserts Panel, Reactions, Search, Preview or SkinTone. There is no composition switch. A full-picker-only layout may omit Panel. For compact reactions mode, place Reactions outside Panel and put the expanded content inside Panel so its hidden/inert state follows the mode.
```tsx check
import * as React from 'react';
import * as Picker from 'emoji-picker-react/primitives';
export function ReactionPicker({ open }: { open: boolean }) {
if (!open) return null;
return (
);
}
```
Conditional mounting controls whether a Root exists. Omit Search/SearchInput, Preview or SkinTone to omit that UI; `open`, `searchDisabled`, `skinTonesDisabled`, `skinTonePickerLocation` and `previewConfig.showPreview` belong to the assembled default picker. `composition` and `panelProps` are unsupported. Put layout and native attributes directly on Panel.
The full primitive grammar, props, refs, native prop forwarding and validation behavior are documented in [PRIMITIVES.md](./PRIMITIVES.md).
### Exports
Parts:
- `Root`
- `Panel` — expanded content with managed hidden/inert presence
- `Reactions` — compact reaction controls
- `Search`
- `SearchInput` — native input / ref-forwarding design-system input
- `LoadError` — localized recoverable loader failure
- `CategoryNav`
- `Viewport`
- `List`
- `Preview`
- `Empty` — renders while an applied search shows nothing (§10a)
- `Loading` — renders while the dataset loads (§12a)
- `SkinTone` — the skin tone control, placed anywhere (§10b)
Hooks (call inside `Root`):
- `useActiveEmoji()` — the hovered/focused emoji, in `onEmojiClick`'s shape
- `useSkinTone()` — `[tone, setTone]`; the setter reports `onSkinToneChange`
- `useSearchState()` — `{ search, resultCount }`
- `useSearchActions()` — `{ setValue, clear }`
- `useCategoryNavigation()` — `{ categories, activeCategory, jumpToCategory }`
- `usePickerMode()` — `{ reactionsOpen, canExpand, expand, collapse }`
- `useEmojiDataState()` — `{ loading, error, retry }`
Tokens (plain data): `structuralPickerTokens`, `lightPickerTokens`, `darkPickerTokens`, `defaultPickerTokens`.
Panel and Reactions are exported parts. Rendering Reactions opts into the compact UI, while `reactionsDefaultOpen`, `allowExpandReactions` and the mode actions control behavior. Rendering a part never requires a secondary presence switch.
## 9. Custom emoji cells and category headers
Branded designs often need different markup, not just different CSS. `List` accepts `components` for emoji cells and category headers. The library still owns their behavior: each component receives the library-owned props and must spread them onto its element.
```tsx
import * as EmojiPicker from 'emoji-picker-react/primitives';
// Define components outside render: a new identity remounts every cell.
function Cell({ emoji, children, ...props }: EmojiPicker.EmojiRenderProps) {
return (
);
}
function Header({ category, ...props }: EmojiPicker.CategoryHeaderRenderProps) {
return