@softwhere-uz/react-native-emoji-keyboard
v0.10.0
Published
Universal (iOS · Android · Web), New-Architecture-first emoji keyboard & reaction picker for React Native and Expo. Bundled Emoji 17.0 data, FlashList v2, deep theming, first-class web parity — one package, no extra data dependency.
Downloads
443
Maintainers
Readme
@softwhere-uz/react-native-emoji-keyboard
A universal (iOS · Android · Web), New-Architecture-first emoji keyboard & reaction picker for React Native and Expo.
Emoji 17.0 · FlashList v2 · deep theming · swappable storage · first-class web parity — a drop-in replacement for the unmaintained rn-emoji-keyboard.
Real screenshots from the example app, verified running on web (react-native-web) and the iOS simulator (Fabric / New Architecture).
Coming from
rn-emoji-keyboard? It has been unmaintained since May 2024 — stuck on Emoji 11.0 (2018) and broken on the web. This package is a drop-in successor: the sameEmojiType/Theme/ prop surface, current Emoji 17.0, and real web parity. Migrating is about four import swaps — see the migration guide.
Why this exists
Most React Native emoji pickers are unmaintained, ship years-old emoji, and break on the web. This library was built to close a real gap: the same component, rendering correctly and identically on iOS, Android, and the web, with current Unicode data and a proper virtualized grid.
| | @softwhere-uz/react-native-emoji-keyboard | rn-emoji-keyboard |
|---|---|---|
| Emoji version | 17.0 (Sept 2025) — 1,914 emoji | 11.0 (2018) |
| Web | first-class parity, no patch needed | broken (empty grid after category change) — needs a userland patch |
| Grid engine | FlashList v2 (virtualized, recycled) | FlatList |
| New Architecture / Fabric | required & verified on device | not guaranteed |
| Theming | deep color-token Theme (restyle-compatible) | color-token Theme |
| Storage | swappable async adapter (SQLite/MMKV/AsyncStorage/localStorage) | built-in only |
| Maintained | actively | last publish 2024-05 |
| Drop-in | EmojiType / Theme / prop-compatible | — |
Features
- 🌍 Truly universal — one codebase for iOS, Android, and web; verified on device.
- 🆕 Emoji 17.0, generated from
emojibase-dataand bundled in — no separate data package. - ⚡ FlashList v2 grid with sticky category headers, jump-to-category, and two-way scroll ↔ tab sync.
- 🎨 Deep theming via a color-token
Themethat matchesrn-emoji-keyboardand@shopify/restyle. - 🎯 Drop-in migration —
EmojiType,Theme, and the prop surface are byte-for-byte compatible (MIGRATION.md). - 👋🏽 Skin tones with a global default + in-picker selector.
- 🔎 Ranked search over names and shortcodes.
- 💾 Bring-your-own storage — the library owns no storage; pass an adapter (or none).
- 🧩 Headless core — the hooks and pure helpers powering the UI are exported for custom pickers.
- 🧱 Composable
EmojiPicker.*primitives —Root/Search/Viewport/List/Empty/Loading/SkinToneSelectorwith overridableCategoryHeader/Row/Emojislots (frimousse-style, adapted to RN). - ♿ Accessibility — arrow-key grid navigation (with a visible focus ring), screen-reader roles, reduced-motion, and RTL-aware horizontal keys.
- 📦 Pluggable data source — ship the full bundled set, a smaller initial slice, or lazy-load/fetch emoji from a
Promise. - 🌐 Bundled locale packs — authoritative CLDR category labels for ~27 locales via a
localeprop. - 🖼️ Image-backed emoji — a consistent cross-OS bundled glyph set (Twemoji), custom (server) emoji, and animated GIF emoji through one resolver.
- 👉 Category swipe gesture + optional cross-fade, web render-support detection (
hideUnsupported), and a pluggable sticker/GIF provider API (MediaPanel). - 🪶 No
reanimatedrequirement in the core path — hard peers are justreact,react-native, and@shopify/flash-list.
Table of contents
- Install
- Quick start
- Usage
- Props
EmojiTypepayload- Theming
- Storage
- Skin tones
- Search
- Bundled data
- Headless / advanced
- Composable primitives
- Keyboard navigation & accessibility
- Async / lazy data
- Localization (locale packs)
- Image-backed emoji (bundled glyph set / custom / animated)
- Category swipe & render-support
- Stickers & GIFs (provider API)
- Compatibility
- How web parity works
- Roadmap
- Development
- License
Install
yarn add @softwhere-uz/react-native-emoji-keyboard @shopify/flash-list
# optional peer — safe-area insets:
yarn add react-native-safe-area-contextThe Emoji 17.0 data ships inside this package — there is no separate data dependency to install.
Peer dependencies
| Peer | Required | Notes |
|---|---|---|
| react | ✅ | >=18 |
| react-native | ✅ | >=0.74, New Architecture |
| @shopify/flash-list | ✅ | >=2.0.0 — the grid engine (v2 is JS-only, so it runs on web) |
| react-native-safe-area-context | optional (>=4.0.0) | Resolved via a module-top try/catch with a zero-inset fallback; omit it and insets are simply 0 |
Quick start
import { EmojiKeyboard, type EmojiType } from '@softwhere-uz/react-native-emoji-keyboard';
export function Picker() {
return <EmojiKeyboard onEmojiSelected={(e: EmojiType) => console.log(e.emoji)} />;
}The keyboard renders inline — give it (or its parent) a height; it is not a modal.
Usage
Composer panel
import { EmojiKeyboard, type EmojiType } from '@softwhere-uz/react-native-emoji-keyboard';
import { View } from 'react-native';
export function EmojiComposerPanel({ panelHeight }: { panelHeight: number }) {
return (
<View style={{ height: panelHeight }}>
<EmojiKeyboard
onEmojiSelected={(emoji: EmojiType) => insertIntoInput(emoji.emoji)}
enableRecentlyUsed
enableSearchBar
categoryPosition="top"
defaultHeight={panelHeight}
/>
</View>
);
}Reaction picker (compact)
import { EmojiKeyboard, type EmojiType } from '@softwhere-uz/react-native-emoji-keyboard';
export function MessageReactionPicker({ onReact }: { onReact: (glyph: string) => void }) {
return (
<EmojiKeyboard
onEmojiSelected={(emoji: EmojiType) => onReact(emoji.emoji)}
hideHeader
disableSafeArea
categoryPosition="top"
defaultHeight={280}
enableRecentlyUsed
/>
);
}Props
EmojiKeyboardProps — every prop is optional except onEmojiSelected.
| Prop | Type | Default | Description |
|---|---|---|---|
| onEmojiSelected | (emoji: EmojiType) => void | required | Fired when the user taps an emoji. EmojiType is byte-for-byte compatible with rn-emoji-keyboard. |
| emojiSize | number | 28 | Glyph font size (px). Also drives the column count when numberOfColumns is unset. |
| hideHeader | boolean | false | Hide the header row (skin-tone selector + customButtons, and the expand knob when expandable). |
| defaultHeight | number \| string | '40%' | Collapsed height of the keyboard (e.g. 280 for a reaction picker, or a '%' string). |
| expandable | boolean | false | Allow the keyboard to expand to expandedHeight via the knob. |
| expandedHeight | number \| string | — | Height when expanded (only meaningful with expandable). |
| categoryPosition | 'top' \| 'bottom' \| 'floating' | 'top' | Where the category tab bar renders relative to the grid. |
| enableRecentlyUsed | boolean | false | Show a leading “Recently used” category. Pass storage to persist it across sessions. |
| enableSearchBar | boolean | false | Show the search input. |
| hideSearchBarClearIcon | boolean | false | Hide the clear (×) icon in the search bar. |
| categoryOrder | CategoryTypes[] | DEFAULT_CATEGORY_ORDER | Override the category order. |
| disabledCategories | CategoryTypes[] | [] | Categories to hide from the tab bar and list. |
| translation | CategoryTranslation | English labels | Per-category localized labels (Record<CategoryTypes, string>). |
| disableSafeArea | boolean | false | Do not apply safe-area insets (useful inside a clipped bottom sheet). |
| selectedEmojis | string[] \| false | false | Glyphs to mark as already selected (highlighted via theme.emoji.selected). |
| allowMultipleSelections | boolean | false | Accepted for rn-emoji-keyboard parity. The inline keyboard never closes on select, so multi-select is expressed through selectedEmojis. |
| theme | RecursivePartial<Theme> | built-in | Color-token theme override (see Theming). |
| styles | RecursivePartial<Styles> | — | Structural ViewStyle/TextStyle overrides per slot. |
| customButtons | React.ReactNode | — | Extra nodes rendered in the header row (e.g. backspace, globe). |
| emojisByCategory | EmojisByCategory[] | bundled data | Provide your own category → emoji data to fully override the dataset. |
| onCategoryChangeFailed | (info) => void | — | Called if a programmatic scroll-to-category fails (FlatList-compatible signature). |
| storage | StorageAdapter | — | First-party. Async storage adapter for recents + skin tone. Omit to disable persistence. |
| numberOfColumns | number | computed from width | First-party. Force a fixed column count. |
| onActiveCategoryChange | (category: CategoryTypes) => void | — | First-party. Fired when the visible category changes via scroll or tab press. |
| defaultSkinTone | SkinTone | 'none' | First-party. Default skin tone applied to tone-enabled emoji. |
| colorScheme | 'light' \| 'dark' \| 'auto' | 'light' | First-party. Base theme; 'auto' follows the OS. theme merges on top. |
| maxEmojiVersion | number | — | First-party. Hide emoji newer than this Emoji spec version (avoids □ “tofu” on older system fonts). |
| shouldInclude | (e: CompactEmoji) => boolean | — | First-party. Per-emoji include predicate (e.g. hide flags). Memoize it. |
| enablePreview | boolean | false | First-party. Show a preview bar (glyph + name) for the emoji under the finger/pointer. |
| enableFavorites | boolean | false | First-party. Leading favorites section + a ⭐ toggle in the long-press popover. |
| emojiSource | EmojiSource | bundled set | First-party. Pluggable/async data source — array, () => list, or () => Promise<list> (see Async / lazy data). |
| locale | string | English | First-party. BCP-47-ish code selecting a bundled category-label pack (e.g. 'es', 'pt-BR'). See Localization. |
| emojiImageResolver | (emoji) => string \| undefined | — | First-party. Render emoji as images (Twemoji glyph set / custom / animated). Pass twemojiImageResolver. |
| enableCategoryChangeGesture | boolean | false | First-party. Horizontal swipe jumps to the prev/next category (no Reanimated). |
| enableCategoryChangeAnimation | boolean | false | First-party. Brief cross-fade on a tab/swipe category jump (honors reduced-motion). |
| hideUnsupported | boolean | false | First-party. Web-only: hide emoji the platform can’t render (canvas probe). No-op on native. |
| recentsMode | 'recency' \| 'frequency' \| 'frecency' | 'recency' | First-party. Rank the leading used-emoji section by recency, usage count, or a blend; non-recency labels it “Frequently used” with a cold-start set. |
| searchDebounceMs | number | 0 | First-party. Debounce applied to the search query. |
| searchMinChars | number | 1 | First-party. Minimum query length before searching. |
| hapticOnSelect | boolean | false | First-party. Short built-in Vibration on select (no native dep; no-op on web). |
| ListComponent | React.ElementType | FlashList | First-party. Swap the virtualized list (e.g. BottomSheetFlatList). |
CategoryTypes: smileys_emotion, people_body, animals_nature, food_drink, travel_places, activities, objects, symbols, flags, plus the virtual recently_used and search.
SkinTone: 'none' | 'light' | 'medium-light' | 'medium' | 'medium-dark' | 'dark'.
EmojiType payload
type EmojiType = {
emoji: string; // the (tone-resolved) glyph
name: string;
slug: string;
unicode_version: string;
toneEnabled: boolean;
alreadySelected?: boolean;
};This matches rn-emoji-keyboard's EmojiType exactly, so existing import type { EmojiType } from 'rn-emoji-keyboard' code keeps working after the swap.
Theming
Pass a (partial) Theme of color tokens. The shape matches rn-emoji-keyboard's Theme exactly and is what a @shopify/restyle theme builder already produces — so an existing restyle-derived object keeps working unchanged.
import { EmojiKeyboard, type Theme } from '@softwhere-uz/react-native-emoji-keyboard';
const theme: Theme = {
backdrop: '#00000055',
knob: '#ffffff',
container: '#ffffff',
header: '#00000099',
skinTonesContainer: '#e3dbcd',
category: { icon: '#000000', iconActive: '#005b96', container: '#e3dbcd', containerActive: '#d1e3ff' },
search: { background: '#00000011', text: '#000000', placeholder: '#00000066', icon: '#00000066' },
customButton: { icon: '#000000', iconPressed: '#005b96', background: '#e3dbcd', backgroundPressed: '#d1e3ff' },
emoji: { selected: '#d1e3ff' },
};
<EmojiKeyboard onEmojiSelected={onSelect} theme={theme} />;Every key is optional via RecursivePartial<Theme>; unspecified tokens fall back to the built-in defaults (also exported as defaultTheme). Use styles for structural ViewStyle/TextStyle overrides of the container, header, knob, category, search bar, and selected-emoji slots.
Storage
The library owns no storage. Pass a StorageAdapter to persist recently-used emoji and the chosen skin tone. All methods may be sync or async:
export interface StorageAdapter {
getItem(key: string): string | null | Promise<string | null>;
setItem(key: string, value: string): void | Promise<void>;
removeItem?(key: string): void | Promise<void>;
}// expo-sqlite (recommended native default)
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabaseSync('emoji-keyboard.db');
db.execSync('CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)');
const sqliteAdapter: StorageAdapter = {
getItem: (k) => db.getFirstSync<{ v: string }>('SELECT v FROM kv WHERE k = ?', [k])?.v ?? null,
setItem: (k, v) => { db.runSync('INSERT OR REPLACE INTO kv (k, v) VALUES (?, ?)', [k, v]); },
removeItem: (k) => { db.runSync('DELETE FROM kv WHERE k = ?', [k]); },
};
// @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
const asyncStorageAdapter: StorageAdapter = {
getItem: (k) => AsyncStorage.getItem(k),
setItem: (k, v) => AsyncStorage.setItem(k, v),
removeItem: (k) => AsyncStorage.removeItem(k),
};
// react-native-mmkv
import { MMKV } from 'react-native-mmkv';
const mmkv = new MMKV();
const mmkvAdapter: StorageAdapter = {
getItem: (k) => mmkv.getString(k) ?? null,
setItem: (k, v) => mmkv.set(k, v),
removeItem: (k) => mmkv.delete(k),
};
// localStorage (web)
const localStorageAdapter: StorageAdapter = {
getItem: (k) => localStorage.getItem(k),
setItem: (k, v) => localStorage.setItem(k, v),
removeItem: (k) => localStorage.removeItem(k),
};<EmojiKeyboard onEmojiSelected={onSelect} enableRecentlyUsed storage={sqliteAdapter} />Need an in-memory adapter for tests or web previews? Import createMemoryAdapter().
Skin tones
Tone-enabled emoji carry the five canonical Fitzpatrick variants (light → dark). Set a global default with defaultSkinTone; the picker also exposes an in-header tone selector. With a storage adapter, the chosen tone is remembered across sessions. (Per-emoji tone memory is on the roadmap.)
<EmojiKeyboard onEmojiSelected={onSelect} defaultSkinTone="medium-dark" storage={adapter} />Search
Enable with enableSearchBar. v0.1 provides ranked English search (prefix → substring) over emoji names and shortcodes from the bundled data. Search is a mode, not a scroll target — while a query is active the grid shows results instead of categories. (Multilingual CLDR search is on the roadmap.)
Bundled data
The compact Emoji 17.0 dataset (1,914 emoji, ~64 KB gzipped) ships inside this package and is re-exported from the main entry, so you can consume the raw data with no extra dependency:
import { emojis, groups, meta } from '@softwhere-uz/react-native-emoji-keyboard';
import type { CompactEmoji, EmojiGroup, EmojiMeta } from '@softwhere-uz/react-native-emoji-keyboard';
meta.emojiVersion; // "17.0"
emojis.length; // 1914CompactEmoji uses short keys to keep the bundle small:
type CompactEmoji = {
e: string; // glyph
n: string; // name / label
g: number; // emojibase group id (0,1,3–9)
o: number; // canonical sort order
k?: string[]; // search keywords
s?: string[]; // shortcodes
v?: number; // emoji spec version (numeric, e.g. 17)
t?: string[]; // 5 tone-variant glyphs, light → dark
};The data is generated from the pinned emojibase-data devDependency via yarn codegen and checked in. Because it lives in the library, a new Unicode version is a library release: bump emojibase-data, run yarn codegen, publish.
Headless / advanced
Building a custom picker? The hooks and pure helpers that power <EmojiKeyboard> are all exported:
import {
// pure helpers
searchEmojis, searchByShortcodePrefix, applyTone, skinToneVariations, toEmojiType, buildGrid, slugify, toneIndex,
// headless emoji-database facade (UI-agnostic: search / autocomplete / fromGlyph / tones)
createEmojiDatabase,
// frecency + reaction history
useFrequentlyUsed, useReactionHistory,
// grid keyboard-navigation model (pure)
nextGridFocus, firstGridFocus, emojiAtFocus, isGridNavKey,
// hooks
useEmojiData, useSearch, useRecents, useSkinTone, useCategorySync, useReveal,
useGridNavigation, useAsyncEmojiData,
// data + utilities
emojis, defaultTheme, createMemoryAdapter,
} from '@softwhere-uz/react-native-emoji-keyboard';
// e.g. a headless "search → resolve tone → payload" pipeline:
const [top] = searchEmojis('rocket', emojis); // CompactEmoji
const glyph = applyTone(top, 'medium'); // "🚀"
const payload = toEmojiType(top, glyph); // EmojiTypeuseReveal is the pure, platform-agnostic requestAnimationFrame reveal hook that makes the web empty-grid bug impossible (details).
Composable primitives
Prefer building your own picker from parts? The EmojiPicker.* namespace exposes a
frimousse-style composable API adapted to React Native. Root
owns all state (search, skin tone, data, keyboard focus); the children read it via context, and the
List slots let you fully restyle each layer while the library keeps virtualization, sticky headers,
the rAF reveal, and keyboard navigation.
import { EmojiPicker } from '@softwhere-uz/react-native-emoji-keyboard';
function Picker({ onPick }: { onPick: (e: EmojiType) => void }) {
return (
<EmojiPicker.Root onEmojiSelect={onPick} columns={9} colorScheme="auto" enableRecentlyUsed>
<EmojiPicker.Search placeholder="Search emoji" />
<EmojiPicker.Viewport>
<EmojiPicker.Loading>{() => <Text>Loading…</Text>}</EmojiPicker.Loading>
<EmojiPicker.Empty>{({ search }) => <Text>No emoji for “{search}”</Text>}</EmojiPicker.Empty>
<EmojiPicker.List
components={{
// every slot is optional — override only what you need
CategoryHeader: ({ label }) => <Text style={styles.header}>{label}</Text>,
Emoji: ({ emoji, onPress, focused }) => (
<Pressable onPress={onPress} style={focused && styles.ring}>
<Text>{emoji.glyph}</Text>
</Pressable>
),
}}
/>
</EmojiPicker.Viewport>
<EmojiPicker.SkinToneSelector />
</EmojiPicker.Root>
);
}Read the live preview emoji from any descendant with EmojiPicker.useActiveEmoji(), and the tone pair
with EmojiPicker.useSkinTone() ([skinTone, setSkinTone]).
Keyboard navigation & accessibility
On web, the grid is a focusable grid landmark: arrow keys move a roving focus (with a visible focus
ring), Home/End jump within the row (Ctrl/Cmd widens to the whole grid), and Enter/Space
select. Under RTL the horizontal arrows follow visual direction. The movement model is a pure,
unit-tested function you can reuse in a custom UI:
import { useGridNavigation, nextGridFocus } from '@softwhere-uz/react-native-emoji-keyboard';
const nav = useGridNavigation(grid); // focus state + move()/focusFirst()/activeEmoji
const next = nextGridFocus(grid, focus, 'ArrowDown'); // pure: { item, col } | nullCells also carry screen-reader roles/labels, accessibilityState, and honor reduced-motion.
Async / lazy data
Pass emojiSource to <EmojiKeyboard> or <EmojiPicker.Root> to ship a smaller initial bundle and
lazy-load the rest, or fetch from a CDN. It accepts an array, a function returning a list, or a
function returning a Promise — while a promise is in flight the grid stays empty and
EmojiPicker.Loading renders. The bundled Emoji 17.0 set stays the synchronous default.
// tiny first paint, then swap in the full set:
const source = useCallback(() => import('./emoji-full').then((m) => m.emojis), []);
<EmojiKeyboard onEmojiSelected={onPick} emojiSource={source} />
// or the headless hook directly:
const { emojis, loading, error } = useAsyncEmojiData(source);Localization (locale packs)
Pass a locale to translate the category labels — bundled, authoritative CLDR strings for ~27 locales
(AVAILABLE_LOCALES). Matching is case-insensitive and falls back from a region tag to the base
language (pt-BR → pt). Your translation prop still overrides per key; the virtual
recently_used / favorites / search labels fall back to English unless you translate them.
<EmojiKeyboard onEmojiSelected={onPick} locale="es" enableSearchBar />
// or a custom pack: getLocalePack('fr'), resolveTranslation('de', { search: 'Suchen' })Localized keyword search (typing a query in your language) is a separate, larger feature and is not bundled — labels only.
Image-backed emoji (bundled glyph set / custom / animated)
Render emoji as images instead of the system font — one mechanism covers three needs:
import { EmojiKeyboard, twemojiImageResolver } from '@softwhere-uz/react-native-emoji-keyboard';
// 1. Consistent, cross-OS glyph set (Twemoji, served from a CDN — no bundled sprite):
<EmojiKeyboard onEmojiSelected={onPick} emojiImageResolver={twemojiImageResolver} />
// 2. Custom / animated emoji via the data override (each carries its own image URL;
// a `.gif` animates where the platform supports it):
<EmojiKeyboard
onEmojiSelected={onPick}
emojisByCategory={[{ title: 'objects', data: [
{ emoji: ':party:', name: 'party', v: '1', toneEnabled: false, img: 'https://cdn/party.gif' },
] }]}
/>A custom emoji's own img URL always wins; otherwise emojiImageResolver(emoji) decides (return
undefined to keep the native glyph). createTwemojiResolver({ format: 'png' }) and twemojiUrl(glyph)
are exported too.
Category swipe & render-support
enableCategoryChangeGesture— a horizontal swipe over the grid jumps to the prev/next category (built-in gesture responder, no Reanimated; vertical scrolling is untouched).enableCategoryChangeAnimationadds a brief cross-fade on tab/swipe jumps (honors reduced-motion).hideUnsupported— on web, canvas-measures whether the platform can actually render each emoji and hides the □-tofu ones. No-op on native/SSR; opt-in and best-effort; composes withshouldInclude+maxEmojiVersion.
Stickers & GIFs (provider API)
The library ships no provider and no API keys — implement MediaProvider against Giphy / Tenor / your
own backend, and drop a MediaPanel (search box + grid) behind your own tab strip. useMediaSearch
handles the debounced query, trending fallback, and race-safe cancellation.
import { MediaPanel, type MediaProvider } from '@softwhere-uz/react-native-emoji-keyboard';
const giphy: MediaProvider = {
id: 'giphy', title: 'GIFs',
search: async (q, { signal } = {}) => (await fetchGiphy(q, signal)).map(toMediaItem),
trending: async () => (await fetchGiphyTrending()).map(toMediaItem),
};
<MediaPanel provider={giphy} onSelect={(item) => insert(item.url)} placeholder="Search GIFs" />Compatibility
| | |
|---|---|
| Platforms | iOS, Android, Web (react-native-web) |
| React Native | >=0.74 — New Architecture required (FlashList v2 is New-Arch-only) |
| React | >=18 |
| Expo | works with the New-Architecture SDKs; verified on Expo SDK 56 / RN 0.85 |
| Verified on device | Web (react-native-web) and iOS (iPhone 16 Pro simulator, Fabric) |
How web parity works
The incumbent gated first paint on a deferred post-interaction callback, which does not reliably fire on react-native-web — leaving the grid empty after a category change. This library makes that impossible by construction:
- The grid engine is FlashList v2 — JS-only (no native module), so the same virtualized grid runs on native and web.
- First paint is revealed via
requestAnimationFrame(neverInteractionManager), and the reveal never re-hides on scroll — so the grid can’t be left blank. A jsdom smoke test guards this in CI. react-native-safe-area-contextis resolved once at module load inside atry/catch, keeping hook identity stable whether or not the peer is installed.
Roadmap
Shipped since v0.1 (tracking issue #1 + the follow-up competitor-research backlog): per-emoji skin-tone memory, emoticon + multi-word ranked search, emoji-version “tofu” gating, reduced-motion + screen-reader semantics, shouldInclude / custom category icons, functional multi-select, auto light/dark theme, preview bar, EmojiModal bottom sheet, ReactionStrip, favorites, composable EmojiPicker.* primitives, keyboard grid-navigation, a pluggable/async data source, bundled locale packs, image-backed emoji (Twemoji glyph set / custom / animated), category swipe gesture, web render-support detection, a pluggable sticker/GIF provider API — plus a frecency “Frequently used” section (recentsMode), smart recent/frequent reactions (ReactionStrip mode), a swappable ListComponent (e.g. BottomSheetFlatList), :shortcode: autocomplete (searchByShortcodePrefix), searchDebounceMs / searchMinChars, opt-in hapticOnSelect, screen-reader result-count announcements, and a headless createEmojiDatabase facade.
- Still open — multilingual keyword search (typing queries in-language; needs bundled per-locale keyword data). On-device polish (arrow-key focus/scroll, RTL) is verified by the maintainer.
- Won't do — a native OS emoji-keyboard mode (iOS
MCEmojiPicker/ Androidemoji2) would require a native module, which breaks Expo Go. This library is intentionally 100% JavaScript (no prebuild, works in Expo Go). If you need the native picker, wrap it in your own app; it won't land in this package.
Development
This is a Yarn-workspaces monorepo (packages/react-native-emoji-keyboard + example).
yarn # install
yarn codegen # regenerate the bundled Emoji data from emojibase-data
yarn typecheck # tsc --noEmit
yarn test # jest (incl. the web-reveal smoke gate)
yarn lint # eslint
yarn build # react-native-builder-bob → CJS + ESM + .d.ts
cd example && yarn && npx expo start # dogfood on iOS / Android / WebLicense
MIT © softwhere-uz
