# expo-native-sheet-emojis
> A fully native emoji picker bottom sheet for React Native (iOS + Android). 60+ FPS, 1900+ emojis (Unicode v16.0), relevance-ranked search across 21 languages powered by Unicode CLDR.
Full documentation: https://github.com/efstathiosntonas/expo-native-sheet-emojis/blob/main/README.md
## Installation
```bash
# Expo
npx expo install expo-native-sheet-emojis
# Bare React Native
yarn add expo-native-sheet-emojis
cd ios && pod install
```
Requires: expo >=54.0.0, react >=18.3.1, react-native >=0.81.0
## Exports
```typescript
import {
EmojiSheetModule, // imperative present()/dismiss()
EmojiSheetView, // declarative component
lightTheme, // light theme preset
darkTheme, // dark theme preset
} from 'expo-native-sheet-emojis';
// All types are also exported:
import type {
EmojiSheetTheme,
EmojiSheetPresentOptions,
EmojiSheetResult,
EmojiSheetTranslations,
EmojiSheetViewProps,
EmojiCategory,
} from 'expo-native-sheet-emojis';
```
## Complete TypeScript Types
```typescript
type EmojiSheetTheme = {
// Required
backgroundColor: string; // main picker background
searchBarBackgroundColor: string; // search input background
textColor: string; // primary text color
textSecondaryColor: string; // secondary/label text color
accentColor: string; // highlights, active states
dividerColor: string; // divider lines
// Optional (have sensible defaults)
searchTextColor?: string; // text inside search input (defaults to textColor)
placeholderTextColor?: string; // placeholder in search (defaults to textSecondaryColor)
selectionColor?: string; // cursor/highlight color
categoryIconColor?: string; // unselected category icons (defaults to textSecondaryColor)
categoryActiveIconColor?: string; // selected category icon (defaults to accentColor)
categoryActiveBackgroundColor?: string; // circle behind selected icon (defaults to dividerColor)
handleColor?: string; // sheet drag handle bar
categoryBarBackgroundColor?: string; // category bar background (defaults to backgroundColor)
};
type EmojiCategory =
| 'frequently_used'
| 'smileys_emotion'
| 'people_body'
| 'animals_nature'
| 'food_drink'
| 'travel_places'
| 'activities'
| 'objects'
| 'symbols'
| 'flags';
type EmojiSheetTranslations = {
searchPlaceholder?: string; // default: "Search emoji"
noResultsText?: string; // default: "No emojis found"
categoryNames?: Partial>; // localized category tab names
};
type EmojiSheetPresentOptions = {
theme?: EmojiSheetTheme | 'dark' | 'light' | 'system'; // default: 'light'
translations?: EmojiSheetTranslations;
snapPoints?: [number, number]; // default: [0.5, 1.0] -- screen fractions
layoutDirection?: 'auto' | 'ltr' | 'rtl'; // default: 'auto' -- follows locale unless forced
categoryBarPosition?: 'top' | 'bottom'; // default: 'top'
columns?: number; // default: 7
emojiSize?: number; // default: 32 (points)
recentLimit?: number; // default: 30
showSearch?: boolean; // default: true
showRecents?: boolean; // default: true
enableSkinTones?: boolean; // default: true
enableHaptics?: boolean; // default: true -- haptic feedback on tap, long-press, skin tone select
enableAnimations?: boolean; // default: false -- scale animation on emoji tap (useful for embedded EmojiSheetView)
onOpen?: () => void; // called when the sheet becomes visible
gestureEnabled?: boolean; // default: true (swipe-to-dismiss)
backdropOpacity?: number; // default: 0.22
excludeEmojis?: string[]; // default: [] -- emoji IDs to hide
};
type EmojiSheetResult =
| { emoji: string; name: string; id: string; cancelled?: never } // user selected an emoji
| { cancelled: true; emoji?: never; name?: never; id?: never }; // user dismissed without selecting
type EmojiSheetViewProps = ViewProps & {
onEmojiSelected: (emoji: string, name: string, id: string) => void; // required callback
onOpen?: () => void; // called when picker becomes visible
onDismiss?: () => void; // called when picker is dismissed (View API only, not Embedded view)
theme?: EmojiSheetTheme | 'dark' | 'light' | 'system';
translations?: EmojiSheetTranslations;
layoutDirection?: 'auto' | 'ltr' | 'rtl';
categoryBarPosition?: 'top' | 'bottom';
columns?: number;
emojiSize?: number;
recentLimit?: number;
showSearch?: boolean;
showRecents?: boolean;
enableSkinTones?: boolean;
enableHaptics?: boolean;
enableAnimations?: boolean;
excludeEmojis?: string[];
};
```
## Imperative API
Present a native bottom sheet and await the user's selection:
```typescript
import { EmojiSheetModule } from 'expo-native-sheet-emojis';
const result = await EmojiSheetModule.present({
theme: 'dark',
layoutDirection: 'auto',
categoryBarPosition: 'bottom',
excludeEmojis: ['pile_of_poo', 'thumbs_down'],
translations: {
searchPlaceholder: 'Find an emoji...',
noResultsText: 'Nothing found',
categoryNames: {
smileys_emotion: 'Smileys',
people_body: 'People',
},
},
});
if (!result.cancelled) {
console.log('Selected:', result.emoji); // e.g. "😀"
}
// Programmatic dismiss
await EmojiSheetModule.dismiss();
// Clear persisted data
await EmojiSheetModule.clearRecents();
await EmojiSheetModule.clearSkinTonePreferences();
```
## Declarative API
Embed the emoji picker inline in your component tree:
```tsx
import { EmojiSheetView } from 'expo-native-sheet-emojis';
console.log(emoji)}
columns={7}
emojiSize={32}
showSearch={true}
showRecents={true}
enableSkinTones={true}
excludeEmojis={['pile_of_poo']}
categoryBarPosition="top"
translations={{
searchPlaceholder: 'Search...',
noResultsText: 'No results',
}}
/>
```
## Theming
```typescript
import { EmojiSheetModule, lightTheme, darkTheme } from 'expo-native-sheet-emojis';
// Use presets
await EmojiSheetModule.present({ theme: 'dark' });
await EmojiSheetModule.present({ theme: darkTheme });
// Custom theme object
await EmojiSheetModule.present({
theme: {
backgroundColor: '#1A1A2E',
searchBarBackgroundColor: '#2A2F39',
textColor: '#FFFFFF',
textSecondaryColor: '#999999',
accentColor: '#EA4578',
dividerColor: '#3A3A4A',
categoryIconColor: '#808080',
categoryActiveIconColor: '#EA4578',
categoryActiveBackgroundColor: '#3A3A4A',
handleColor: '#666666',
categoryBarBackgroundColor: '#1A1A2E',
},
});
```
### Light Theme Preset Values
```typescript
const lightTheme: EmojiSheetTheme = {
backgroundColor: '#FFFFFF',
searchBarBackgroundColor: '#F0F0F0',
textColor: '#000000',
textSecondaryColor: '#808080',
accentColor: '#EA4578',
dividerColor: '#E0E0E0',
categoryIconColor: '#666666',
categoryActiveIconColor: '#EA4578',
categoryActiveBackgroundColor: '#E0E0E0',
handleColor: '#B8B8B8',
categoryBarBackgroundColor: '#FFFFFF',
};
```
### Dark Theme Preset Values
```typescript
const darkTheme: EmojiSheetTheme = {
backgroundColor: '#1A1A2E',
searchBarBackgroundColor: '#2A2F39',
textColor: '#FFFFFF',
textSecondaryColor: '#999999',
accentColor: '#EA4578',
dividerColor: '#3A3A4A',
categoryIconColor: '#808080',
categoryActiveIconColor: '#EA4578',
categoryActiveBackgroundColor: '#3A3A4A',
handleColor: '#666666',
categoryBarBackgroundColor: '#1A1A2E',
};
```
## Multilingual Search
English keywords are always included. No extra locales are bundled by default -- opt in per platform:
**Expo (managed/prebuild):** Config plugin in app.json:
```json
{ "plugins": [["expo-native-sheet-emojis", { "searchLocales": ["es", "fr", "de", "ja"] }]] }
```
Run `npx expo prebuild --clean` after configuring.
**Bare React Native:**
- Android: No setup needed -- Gradle copies all translations automatically at build time.
- iOS: Add a `pre_install` hook in your Podfile to copy locale files during `pod install`. See README for snippet.
Supported locales: ca, cs, de, el, en, es, fi, fr, hi, hu, it, ja, ko, nl, pl, pt, ru, sv, tr, uk, zh
Each locale adds ~64-185KB. All 21 locales total ~2.3MB.
## Emoji IDs
Emoji IDs are snake_case derived from the Unicode name. Used in `excludeEmojis` and returned in the native event data.
Examples: `grinning_face`, `thumbs_up`, `thumbs_down`, `pile_of_poo`, `red_heart`, `rocket`, `fire`, `party_popper`, `waving_hand`, `thinking_face`
## Category Bar Position
- **top** (default): Inline strip below search bar, mirrors native keyboard layout
- **bottom**: Floating rounded pill with blur backdrop (iOS) / elevation shadow (Android), hides during search
## Layout Direction
- **auto** (default): Follows the device locale/layout direction
- **ltr**: Forces a left-to-right layout
- **rtl**: Forces a right-to-left layout
This applies to the search bar, category strip, sticky category headers, and surrounding picker chrome.
## Search Behavior
Search is relevance-ranked with the following scoring:
| Score | Match Type |
|-|-|
| 100 | Exact emoji name match |
| 90 | Emoji name starts with query |
| 80 | Exact keyword match |
| 70 | Keyword starts with query |
| 50 | Emoji name contains query |
| 30 | Keyword contains query |
| 10 | Localized keyword contains query |
Search runs on a background thread. Results scroll to top automatically.
## Key Behaviors
- Frequently used emojis persist across app launches (UserDefaults on iOS, SharedPreferences on Android)
- Skin tone preferences persist per emoji
- Frequently used section only appears when there are actual entries
- Bottom floating pill hides during search (keyboard visible)
- All data loading and search runs on background threads
- Emoji data and translation keywords are cached in memory after first load
- Clear (X) button appears in search bar when text is entered
- Custom translation files can be created as JSON: `{ "emoji_char": ["keyword1", "keyword2"] }`