Building an EmojiEmoji
Mot japonais (絵文字) signifiant 'caractère image' — petits symboles graphiques utilisés dans la communication numérique pour exprimer des idées, des émotions et des objets. Picker Component from Scratch
Emoji pickers look simple on the surface, but building one that feels fast and accessible involves decisions about data management, virtualization, skin tone state, keyboard navigation, and search. This guide covers the key architectural decisions and implementation patterns.
Data Sourcing
The foundation of any emoji picker is a structured dataset. Good options include:
UnicodeUnicode
Standard universel d'encodage des caractères qui attribue un numéro unique à chaque caractère de tous les systèmes d'écriture et ensembles de symboles, y compris les emoji. CLDRCLDR (CLDR)
Le Common Locale Data Repository, un projet Unicode fournissant des données spécifiques aux paramètres régionaux, notamment les noms d'emoji et les mots-clés de recherche dans plus de 100 langues. Data
The Common Locale Data Repository includes emoji annotations (short names, keywords) in 60+ languages. The emoji-test.txtemoji-test.txt file from the Unicode UCD provides the canonical ordered list of all fully-qualified emoji.
Le fichier officiel Unicode répertoriant toutes les séquences emoji avec leur statut de qualification, leurs points de code et leurs noms abrégés CLDR.
# Download the emoji test file
curl -O https://unicode.org/Public/emoji/latest/emoji-test.txt
emoji-datasource
The emoji-datasource npm package provides pre-processed JSON with categories, names, skin tone variants, and platform image URLs:
npm install emoji-datasource
import data from 'emoji-datasource/emoji.json';
// Structure of each entry:
// {
// name: "GRINNING FACE",
// unified: "1F600",
// short_names: ["grinning"],
// category: "Smileys & People",
// sort_order: 1,
// skin_variations: {
// "1F3FB": { unified: "1F600-1F3FB", ... },
// ...
// }
// }
// Build a usable array
const emojis = data.map(e => ({
emoji: String.fromCodePoint(...e.unified.split('-').map(h => parseInt(h, 16))),
name: e.name.toLowerCase(),
keywords: e.short_names,
category: e.category,
skinVariations: e.skin_variations,
}));
Component Architecture
A minimal emoji picker has these layers:
EmojiPicker
├── SearchBar
├── CategoryNav (tabs or sidebar)
├── EmojiGrid (virtualized)
│ └── EmojiButton × N
├── SkinTonePicker
└── Preview (hovered emoji name)
React Implementation
import { useState, useMemo, useCallback, useRef } from 'react';
import { FixedSizeGrid } from 'react-window';
import emojis from './emoji-data';
const COLUMN_COUNT = 8;
const CELL_SIZE = 40;
export function EmojiPicker({ onSelect }) {
const [query, setQuery] = useState('');
const [skinTone, setSkinTone] = useState(null); // null = default
const [category, setCategory] = useState('all');
const [hovered, setHovered] = useState(null);
const filtered = useMemo(() => {
let list = emojis;
if (category !== 'all') {
list = list.filter(e => e.category === category);
}
if (query.trim()) {
const q = query.toLowerCase();
list = list.filter(e =>
e.name.includes(q) || e.keywords.some(k => k.includes(q))
);
}
return list;
}, [query, category]);
const rows = useMemo(() => {
const result = [];
for (let i = 0; i < filtered.length; i += COLUMN_COUNT) {
result.push(filtered.slice(i, i + COLUMN_COUNT));
}
return result;
}, [filtered]);
const getEmoji = useCallback((item) => {
if (skinTone && item.skinVariations?.[skinTone]) {
const unified = item.skinVariations[skinTone].unified;
return String.fromCodePoint(...unified.split('-').map(h => parseInt(h, 16)));
}
return item.emoji;
}, [skinTone]);
const Cell = useCallback(({ columnIndex, rowIndex, style }) => {
const item = rows[rowIndex]?.[columnIndex];
if (!item) return <div style={style} />;
const char = getEmoji(item);
return (
<div style={style}>
<button
className="emoji-btn"
onClick={() => onSelect(char)}
onMouseEnter={() => setHovered(item.name)}
onMouseLeave={() => setHovered(null)}
aria-label={item.name}
title={item.name}
>
{char}
</button>
</div>
);
}, [rows, getEmoji, onSelect]);
return (
<div className="emoji-picker" role="dialog" aria-label="Emoji picker">
<SearchBar value={query} onChange={setQuery} />
<CategoryNav value={category} onChange={setCategory} />
<FixedSizeGrid
columnCount={COLUMN_COUNT}
columnWidth={CELL_SIZE}
rowCount={rows.length}
rowHeight={CELL_SIZE}
height={320}
width={COLUMN_COUNT * CELL_SIZE}
>
{Cell}
</FixedSizeGrid>
<div className="picker-footer">
<SkinTonePicker value={skinTone} onChange={setSkinTone} />
{hovered && <span className="preview-name">{hovered}</span>}
</div>
</div>
);
}
Virtualization
A full emoji dataset has 3,700+ characters. Rendering all of them as DOM nodes simultaneously causes layout thrashing. Use a virtual list/grid:
react-window: Lightweight, renders only visible cells@tanstack/virtual: Framework-agnostic, more flexible- CSS
content-visibility: auto: A CSS-only approach for simpler cases
/* CSS-only approach for moderate emoji counts */
.emoji-grid {
display: grid;
grid-template-columns: repeat(8, 40px);
}
.emoji-btn {
content-visibility: auto;
contain-intrinsic-size: 40px 40px;
}
Search Implementation
Fast local search requires pre-processing at startup:
// Pre-build a search index at initialization
const searchIndex = emojis.map(e => ({
...e,
searchString: [e.name, ...e.keywords].join(' ').toLowerCase(),
}));
function searchEmoji(query) {
if (!query.trim()) return emojis;
const q = query.toLowerCase().trim();
// Prioritize name matches, then keyword matches
const nameMatches = searchIndex.filter(e => e.name.includes(q));
const kwMatches = searchIndex.filter(
e => !e.name.includes(q) && e.keywords.some(k => k.includes(q))
);
return [...nameMatches, ...kwMatches];
}
For fuzzy search, integrate a library like fuse.js:
import Fuse from 'fuse.js';
const fuse = new Fuse(emojis, {
keys: ['name', 'keywords'],
threshold: 0.3,
includeScore: true,
});
function fuzzySearch(query) {
return fuse.search(query).map(r => r.item);
}
Skin Tone State
Skin tone preferences should persist across sessions:
const SKIN_TONE_KEY = 'emoji-picker-skin-tone';
// Fitzpatrick modifiers
const SKIN_TONES = {
default: null,
light: '1F3FB', // 🏻
mediumLight: '1F3FC', // 🏼
medium: '1F3FD', // 🏽
mediumDark: '1F3FE', // 🏾
dark: '1F3FF', // 🏿
};
function useSkinTone() {
const [tone, setTone] = useState(
() => localStorage.getItem(SKIN_TONE_KEY) || null
);
const updateTone = useCallback((newTone) => {
setTone(newTone);
if (newTone) {
localStorage.setItem(SKIN_TONE_KEY, newTone);
} else {
localStorage.removeItem(SKIN_TONE_KEY);
}
}, []);
return [tone, updateTone];
}
Keyboard Navigation
Keyboard navigation is essential for accessibility and power users:
function useGridKeyboard({ columns, rowCount, onSelect }) {
const [focused, setFocused] = useState({ row: 0, col: 0 });
const handleKeyDown = useCallback((e) => {
const { row, col } = focused;
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
setFocused({ row, col: Math.min(col + 1, columns - 1) });
break;
case 'ArrowLeft':
e.preventDefault();
setFocused({ row, col: Math.max(col - 1, 0) });
break;
case 'ArrowDown':
e.preventDefault();
setFocused({ row: Math.min(row + 1, rowCount - 1), col });
break;
case 'ArrowUp':
e.preventDefault();
setFocused({ row: Math.max(row - 1, 0), col });
break;
case 'Enter':
case ' ':
e.preventDefault();
onSelect(focused);
break;
}
}, [focused, columns, rowCount, onSelect]);
return { focused, handleKeyDown };
}
Performance Tips
- Lazy-load category data: Only parse emojis for the active category
- Memoize filtered results: Avoid re-filtering on unrelated state changes
- Debounce search input: 150–200ms debounce prevents excessive filtering
- Use CSS sprite sheets: Platform-specific emoji image sets (Twitter, Google) ship as sprite sheets for faster loading than individual files
import { useDeferredValue, useMemo } from 'react';
function EmojiPicker({ onSelect }) {
const [rawQuery, setRawQuery] = useState('');
// Defer search processing to avoid blocking input
const query = useDeferredValue(rawQuery);
const results = useMemo(() => searchEmoji(query), [query]);
// ...
}
Explore More on EmojiFYI
- Try the native keyboard-based emoji tool: Emoji Keyboard
- Analyze emoji sequences used in your picker: Sequence Analyzer
- Compare how emoji look across platforms: Compare Tool
- Access structured emoji data for your picker: API Reference