A flexible, event-driven emoji picker library for the web.
Zero dependencies. Full developer control. Framework agnostic. https://pfurpass.github.io/EmojiPicker/
const picker = new EmojiPicker({ container: '#btn' })
picker.on('emojiClick', (emoji) => console.log(emoji.char)) // π- Installation
- Setup β Two Files
- Quick Start
- Configuration Options
- Events
- Methods
- Emoji Object
- Modes
- Skin Tone Support
- Theming & CSS Variables
- Custom Emojis
- Helper: attachToInput()
- Favorites & getTopFavorites()
- i18n / Localization
- TypeScript
- Framework Integration
- Recipes / Examples
- Accessibility
- Browser Support
- How It Works Internally
npm install @schwitzerskills/emojipicker<script src="https://cdn.jsdelivr.net/npm/@schwitzerskills/emojipicker/emoji-picker.js"></script>Important: The library needs two files to work.
| File | Purpose |
|---|---|
emoji-picker.js |
The picker core (~5 KB) |
emoji-data.json |
All emoji data (~850 KB, loaded once, cached forever) |
Both files must be accessible at the same URL path. The library auto-detects emoji-data.json relative to its own <script src> tag.
If you use npm / a bundler, copy emoji-data.json to your public/static folder and pass the URL manually:
new EmojiPicker({
container: '#btn',
dataUrl: '/static/emoji-data.json' // or your CDN URL
})If you use CDN, both files are already on jsDelivr β no config needed:
<script src="https://cdn.jsdelivr.net/npm/@schwitzerskills/emojipicker/emoji-picker.js"></script>How the caching works:
- First visit β
emoji-data.jsonis fetched once (~850 KB) - Data is stored in IndexedDB on the user's device
- Every visit after that β loaded from IndexedDB in milliseconds, zero network request
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
</head>
<body>
<input type="text" id="message" placeholder="Type a message...">
<button id="emoji-btn">π</button>
<script src="https://cdn.jsdelivr.net/npm/@schwitzerskills/emojipicker/emoji-picker.js"></script>
<script>
const picker = new EmojiPicker({ container: '#emoji-btn' })
picker.on('emojiClick', (emoji) => {
document.querySelector('#message').value += emoji.char
})
</script>
</body>
</html>import EmojiPicker from '@schwitzerskills/emojipicker'
const picker = new EmojiPicker({
container: '#emoji-btn',
dataUrl: '/public/emoji-data.json' // adjust to your setup
})
picker.on('emojiClick', (emoji) => {
document.querySelector('#message').value += emoji.char
})import EmojiPicker from '@schwitzerskills/emojipicker'
// Automatically adds a π button and inserts emoji at cursor
EmojiPicker.attachToInput('#message')All properties are optional.
const picker = new EmojiPicker({
container: '#my-button',
theme: 'auto',
mode: 'dropdown',
locale: 'en',
search: true,
recentEmojis: true,
maxRecent: 24,
skinTone: 'default',
customEmojis: [],
perRow: 8,
emojiSize: 28,
autoClose: true,
dataUrl: null,
})| Option | Type | Default | Description |
|---|---|---|---|
container |
string | HTMLElement |
null |
Trigger element β toggles picker on click |
theme |
string |
'auto' |
'light', 'dark', or 'auto' |
mode |
string |
'dropdown' |
See Modes |
locale |
string |
'en' |
UI language β see i18n |
search |
boolean |
true |
Show/hide search input |
recentEmojis |
boolean |
true |
Track recents in IndexedDB |
maxRecent |
number |
24 |
Max recent emojis to store |
skinTone |
string |
'default' |
Default skin tone |
customEmojis |
array |
[] |
Custom emoji definitions |
perRow |
number |
8 |
Grid columns |
emojiSize |
number |
28 |
Emoji size in px (--ep-size) |
autoClose |
boolean |
true |
Close after selecting |
dataUrl |
string |
null |
Custom URL to emoji-data.json |
picker.on(eventName, handler) // add listener
picker.off(eventName, handler) // remove listenerFired when the user selects an emoji. The main event you'll use.
picker.on('emojiClick', (emoji, mouseEvent) => {
console.log(emoji.char) // "π"
console.log(emoji.name) // "face_with_tears_of_joy"
console.log(emoji.category) // "Smileys & Emotion"
console.log(emoji.unicode) // "1F602"
console.log(emoji.skinTone) // null | "medium" | ...
})Fired when hovering over an emoji.
picker.on('emojiHover', (emoji, mouseEvent) => {
myPreview.textContent = emoji.char + ' ' + emoji.name
})picker.on('pickerOpen', () => console.log('opened'))
picker.on('pickerClose', () => console.log('closed'))picker.on('categoryChange', ({ category }) => {
console.log('Switched to:', category) // "Food & Drink"
})picker.on('search', ({ query }) => {
console.log('User typed:', query)
})All methods return this (chainable), except destroy() and the async methods.
picker.open() // open
picker.close() // close
picker.toggle() // toggle
picker.setTheme('dark') // switch theme
picker.setLocale('de') // switch language
picker.destroy() // remove from DOM, clean up listenersAsync methods:
// Returns top N most-clicked emojis
const favs = await picker.getTopFavorites(8)
// β [{ name, char, count }, ...]
// Clear recent history
await picker.clearRecent()
// Clear favorite click counts
await picker.clearFavorites()Static methods:
// Attach to any input (see section below)
EmojiPicker.attachToInput('#message', opts)
// Pre-warm: fetch + cache data without showing any UI
// Call this on app startup so first open is instant
await EmojiPicker.preload({ dataUrl: '/static/emoji-data.json' })Chaining:
new EmojiPicker({ container: '#btn' })
.on('emojiClick', (e) => insertEmoji(e.char))
.on('pickerOpen', () => analytics.track('picker_opened'))
.on('pickerClose', () => analytics.track('picker_closed'))Every emoji-related event provides this structure:
{
char: "ππ½", // emoji character, skin tone applied
name: "thumbs_up", // snake_case identifier
category: "People & Body", // category name
unicode: "1F44D", // base code point (hex)
skinTone: "medium", // null if default
isCustom: false // true for custom image emojis
}Floating panel anchored to the trigger element. Closes on outside click or Esc.
new EmojiPicker({ container: '#btn', mode: 'dropdown' })Always visible, embedded inside the container. autoClose is ignored.
new EmojiPicker({ container: '#my-div', mode: 'inline', autoClose: false })<div id="my-div"></div> <!-- Picker renders here -->Centers in the viewport β ideal for modals or custom trigger logic.
const picker = new EmojiPicker({ mode: 'popup' })
document.getElementById('btn').addEventListener('click', () => picker.open())Users can pick a skin tone in the footer. Set a default in options:
new EmojiPicker({ skinTone: 'medium-dark' })| Value | Example |
|---|---|
'default' |
π |
'light' |
ππ» |
'medium-light' |
ππΌ |
'medium' |
ππ½ |
'medium-dark' |
ππΎ |
'dark' |
ππΏ |
.ep-picker {
--ep-bg: #16192a; /* picker background */
--ep-surface: #1e2236; /* surface / hover bg */
--ep-border: rgba(255,255,255,0.07);
--ep-text: #e2e6f5;
--ep-text-dim: #636b86;
--ep-accent: #6c63ff; /* active tab, focus rings */
--ep-hover: rgba(108,99,255,0.13);
--ep-size: 28px; /* emoji size */
--ep-radius: 18px; /* picker border-radius */
}new EmojiPicker({ theme: 'light' }) // light
new EmojiPicker({ theme: 'dark' }) // dark
new EmojiPicker({ theme: 'auto' }) // follows OS
picker.setTheme('dark') // switch at runtime.ep-picker {
--ep-accent: #e91e8c;
--ep-hover: rgba(233,30,140,0.10);
--ep-active-tab: rgba(233,30,140,0.18);
}Add your own GIFs, PNGs or SVGs alongside the standard set:
new EmojiPicker({
customEmojis: [
{ name: 'party_parrot', url: '/assets/parrot.gif' },
{ name: 'company_logo', url: '/assets/logo.png' },
{ name: 'custom_star', url: 'https://cdn.example.com/star.svg' }
]
})They appear in a dedicated Custom tab. Click event returns:
{ char: null, name: 'party_parrot', category: 'custom', isCustom: true }Wraps any <input> or <textarea> and handles cursor-position insertion automatically.
// Basic
EmojiPicker.attachToInput('#message')
// With options
EmojiPicker.attachToInput('#chat-box', {
theme: 'dark',
skinTone: 'medium',
dataUrl: '/static/emoji-data.json'
})
// Returns the picker instance
const picker = EmojiPicker.attachToInput('#editor')
picker.on('emojiClick', () => updateCharCount())Every emoji click is counted and stored in IndexedDB. Use this to build "most used" sections, reaction quick-bars, or analytics.
// Get top 8 most-clicked emojis
const favs = await picker.getTopFavorites(8)
// β [{ name: 'thumbs_up', char: 'π', count: 42 }, ...]
// Render a quick-access bar
favs.forEach(({ char }) => {
const btn = document.createElement('button')
btn.textContent = char
quickBar.appendChild(btn)
})
// Reset counts
await picker.clearFavorites()| Code | Language |
|---|---|
en |
English (default) |
de |
German |
fr |
French |
es |
Spanish |
pt |
Portuguese |
ja |
Japanese |
// Set at construction
new EmojiPicker({ locale: 'de' })
// Switch at runtime (re-renders if open)
picker.setLocale('fr')EmojiPicker.LOCALES['nl'] = {
search: 'Zoek emojiβ¦',
noResults: 'Geen resultaten voor',
noRecent: 'Nog geen recente emojis',
recent: 'Recent gebruikt',
custom: 'Aangepast',
loading: 'Ladenβ¦',
categories: {
recent: 'Recent', 'Smileys & Emotion': 'Smileys', 'People & Body': 'Mensen',
'Animals & Nature': 'Natuur', 'Food & Drink': 'Eten', Activities: 'Activiteiten',
'Travel & Places': 'Reizen', Objects: 'Objecten', Symbols: 'Symbolen',
Flags: 'Vlaggen', custom: 'Aangepast'
},
skinTones: {
default: 'Standaard', light: 'Licht', 'medium-light': 'Medium licht',
medium: 'Medium', 'medium-dark': 'Medium donker', dark: 'Donker'
}
}
new EmojiPicker({ locale: 'nl' })The package ships with a .d.ts file. No @types/ package needed.
import EmojiPicker, { EmojiObject, EmojiPickerOptions, FavoriteEmoji } from '@schwitzerskills/emojipicker'
const options: EmojiPickerOptions = {
container: '#btn',
theme: 'auto',
locale: 'de',
dataUrl: '/static/emoji-data.json'
}
const picker = new EmojiPicker(options)
picker.on('emojiClick', (emoji: EmojiObject) => {
console.log(emoji.char, emoji.name)
})
const favs: FavoriteEmoji[] = await picker.getTopFavorites(10)import { useEffect, useRef } from 'react'
import EmojiPicker, { EmojiObject } from '@schwitzerskills/emojipicker'
interface Props {
onSelect: (emoji: EmojiObject) => void
}
export function EmojiButton({ onSelect }: Props) {
const btnRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (!btnRef.current) return
const picker = new EmojiPicker({
container: btnRef.current,
theme: 'auto',
dataUrl: '/static/emoji-data.json'
})
picker.on('emojiClick', onSelect)
return () => picker.destroy()
}, [onSelect])
return <button ref={btnRef} type="button">π</button>
}EmojiPicker uses window and document internally, so it must only run on the client.
Option A β dynamic import (recommended):
// components/EmojiButton.tsx β client-only wrapper
'use client'
import { useEffect, useRef } from 'react'
import EmojiPicker from '@schwitzerskills/emojipicker'
export default function EmojiButton({ onSelect }) {
const btnRef = useRef(null)
useEffect(() => {
const picker = new EmojiPicker({ container: btnRef.current, dataUrl: '/emoji-data.json' })
picker.on('emojiClick', onSelect)
return () => picker.destroy()
}, [onSelect])
return <button ref={btnRef}>π</button>
}// app/page.tsx or pages/index.tsx
import dynamic from 'next/dynamic'
const EmojiButton = dynamic(() => import('../components/EmojiButton'), { ssr: false })Option B β App Router 'use client' directive:
'use client'
// useEffect only runs in the browser β safe without ssr:false<template>
<button ref="btnRef">π</button>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import EmojiPicker, { EmojiObject } from '@schwitzerskills/emojipicker'
const emit = defineEmits<{ select: [emoji: EmojiObject] }>()
const btnRef = ref<HTMLButtonElement | null>(null)
let picker: EmojiPicker | null = null
onMounted(() => {
picker = new EmojiPicker({ container: btnRef.value!, dataUrl: '/emoji-data.json' })
picker.on('emojiClick', (emoji) => emit('select', emoji))
})
onUnmounted(() => picker?.destroy())
</script><script lang="ts">
import { onMount, onDestroy, createEventDispatcher } from 'svelte'
import EmojiPicker from '@schwitzerskills/emojipicker'
const dispatch = createEventDispatcher()
let btnEl: HTMLButtonElement
let picker: EmojiPicker
onMount(() => {
picker = new EmojiPicker({ container: btnEl, dataUrl: '/emoji-data.json' })
picker.on('emojiClick', (emoji) => dispatch('select', emoji))
})
onDestroy(() => picker?.destroy())
</script>
<button bind:this={btnEl}>π</button>const textarea = document.querySelector('#editor')
const picker = new EmojiPicker({ container: '#btn' })
picker.on('emojiClick', (emoji) => {
const s = textarea.selectionStart
const e = textarea.selectionEnd
textarea.value =
textarea.value.slice(0, s) + emoji.char + textarea.value.slice(e)
textarea.setSelectionRange(s + emoji.char.length, s + emoji.char.length)
textarea.focus()
})picker.on('emojiClick', (emoji) => {
navigator.clipboard.writeText(emoji.char).then(() => showToast('Copied!'))
})picker.on('emojiClick', (emoji) => {
fetch('/api/reactions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messageId, emoji: emoji.name, char: emoji.char })
})
})// Call once at app init β data is fetched and cached in IndexedDB.
// Every picker opened after this is instant.
EmojiPicker.preload({ dataUrl: '/static/emoji-data.json' })const bar = document.getElementById('quick-bar')
async function renderFavBar() {
const favs = await picker.getTopFavorites(6)
bar.innerHTML = favs.map(({ char, name }) =>
`<button title="${name}" onclick="insertEmoji('${char}')">${char}</button>`
).join('')
}
picker.on('pickerClose', renderFavBar)picker.on('pickerOpen', () => analytics.track('picker_opened'))
picker.on('emojiClick', ({ name }) => analytics.track('emoji_used', { name }))
picker.on('search', ({ query }) => analytics.track('emoji_search', { query }))
picker.on('categoryChange', ({ category }) => analytics.track('category_view', { category }))role="dialog"+aria-modal="true"on the pickerrole="tablist"on category tabs,aria-selectedon active tabrole="grid"+role="gridcell"on emoji gridaria-labelon every emoji buttonaria-live="polite"on category label and previewEsccloses the picker- Focus moves to the search input on open
type="button"on all buttons β safe inside<form>elements
| Browser | Version |
|---|---|
| Chrome | 80+ |
| Firefox | 78+ |
| Safari | 14+ |
| Edge | 80+ |
| iOS Safari | 14+ |
| Android Chrome | 80+ |
Requires IndexedDB for caching β available in all modern browsers, gracefully degraded if blocked (e.g. Firefox private mode with resistFingerprinting).
First visit:
emoji-picker.js (~5 KB) loads instantly
β
picker.open() is called
β
emoji-data.json is fetched (~850 KB, one time only)
β
data is stored in IndexedDB
Every visit after:
emoji-picker.js loads
β
picker.open() β data loads from IndexedDB in <5ms
β
zero network request for emoji data
IndexedDB stores:
| Store | Contents | Key |
|---|---|---|
cache |
Full emoji data JSON | 'emojidata' |
recent |
Last used emojis + timestamps | name |
favorites |
Click counts per emoji | name |
Emoji support detection:
The library uses a canvas-based test to detect which Unicode Emoji version the OS supports, then hides emojis that would render as broken boxes. Tests run once per session and are cached in memory.
No separate ESM build is needed. The UMD bundle handles require(), import, and <script> tags.
{
"name": "@schwitzerskills/emojipicker",
"version": "2.0.0",
"main": "emoji-picker.js",
"browser": "emoji-picker.js",
"types": "emoji-picker.d.ts",
"exports": {
".": {
"require": "./emoji-picker.js",
"import": "./emoji-picker.js",
"types": "./emoji-picker.d.ts"
}
},
"files": [
"emoji-picker.js",
"emoji-picker.d.ts",
"emoji-data.json"
]
}Apache