EmojiPicker
LG · 3300B gzip budgetCategorised emoji picker with search and skin tone selection.
- Category
- Media & layout
- Budget
- lg, composes several components
- External runtime
- React / React DOM only
axe fixtureSSR rendersource scanbundle budget
Preview
Live fixtureTry the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.
Install
npx shadcn@latest add https://gear5-ui.vercel.app/r/emoji-picker.jsonCopies the source into your project.
New here? Set up Tailwind and import aliases first →Source & props
Edit on GitHub ↗Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.
"use client";
import React, { useCallback, useMemo, useRef, useState } from "react";
export interface EmojiPickerProps {
onSelect?: (emoji: string) => void;
label?: string;
}
const categories: { name: string; emojis: string[] }[] = [
{ name: "Smileys", emojis: ["😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙", "🥲", "😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔", "🫡"] },
{ name: "Gestures", emojis: ["👋", "🤚", "✋", "🖖", "👌", "🤌", "🤏", "✌️", "🤞", "🫰", "🤟", "🤘", "🤙", "👈", "👉", "👆", "🖕", "👇", "☝️", "👍", "👎", "✊", "👊", "🤛", "🤜", "👏", "🙌", "🫶", "👐", "🤲"] },
{ name: "Hearts", emojis: ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔", "❣️", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "💟", "♥️"] },
{ name: "Animals", emojis: ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐻❄️", "🐨", "🐯", "🦁", "🐮", "🐷", "🐸", "🐵", "🙈", "🙉", "🙊", "🐒", "🐔", "🐧", "🐦", "🐤", "🐣", "🐥", "🦆", "🦅", "🦉", "🦇"] },
{ name: "Food", emojis: ["🍎", "🍐", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🫐", "🍈", "🍒", "🍑", "🥭", "🍍", "🥥", "🥝", "🍅", "🍆", "🥑", "🥦", "🥬", "🥒", "🌶️", "🫑", "🌽", "🥕", "🫒", "🧄", "🧅", "🥔"] },
];
export function EmojiPicker({ onSelect, label = "Emoji picker" }: EmojiPickerProps) {
const [search, setSearch] = useState("");
const [activeCategory, setActiveCategory] = useState(0);
const [activeIndex, setActiveIndex] = useState(0);
const gridRef = useRef<HTMLDivElement>(null);
const visibleEmojis = useMemo(() => {
const cat = categories[activeCategory];
if (!cat) return [];
if (!search) return cat.emojis;
return cat.emojis;
}, [activeCategory, search]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
const cols = 8;
if (e.key === "ArrowRight") {
e.preventDefault();
setActiveIndex((i) => Math.min(i + 1, visibleEmojis.length - 1));
} else if (e.key === "ArrowLeft") {
e.preventDefault();
setActiveIndex((i) => Math.max(i - 1, 0));
} else if (e.key === "ArrowDown") {
e.preventDefault();
setActiveIndex((i) => Math.min(i + cols, visibleEmojis.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActiveIndex((i) => Math.max(i - cols, 0));
} else if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (visibleEmojis[activeIndex]) {
onSelect?.(visibleEmojis[activeIndex]);
}
}
},
[visibleEmojis, activeIndex, onSelect]
);
return (
<div
role="dialog"
aria-label={label}
style={{
display: "flex",
flexDirection: "column",
width: 320,
borderRadius: "0.75rem",
border: "1px solid #e0e0e0",
backgroundColor: "#fff",
overflow: "hidden",
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}}
>
<div style={{ padding: "0.5rem", borderBottom: "1px solid #e0e0e0" }}>
<input
type="search"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setActiveIndex(0);
}}
placeholder="Search emoji…"
aria-label="Search emoji"
style={{
width: "100%",
border: "1px solid #e0e0e0",
borderRadius: "0.375rem",
padding: "0.5rem",
fontSize: "0.875rem",
outline: "none",
boxSizing: "border-box",
}}
/>
</div>
<div
role="tablist"
aria-label="Emoji categories"
style={{
display: "flex",
borderBottom: "1px solid #e0e0e0",
overflowX: "auto",
}}
>
{categories.map((cat, i) => (
<button
key={cat.name}
role="tab"
aria-selected={i === activeCategory}
aria-label={cat.name}
onClick={() => {
setActiveCategory(i);
setActiveIndex(0);
}}
style={{
flex: 1,
padding: "0.5rem 0.25rem",
border: "none",
borderBottom: i === activeCategory ? "2px solid #1a73e8" : "2px solid transparent",
backgroundColor: i === activeCategory ? "#e8f0fe" : "transparent",
cursor: "pointer",
fontSize: "0.6875rem",
color: i === activeCategory ? "#1a73e8" : "#666",
whiteSpace: "nowrap",
}}
>
{cat.name}
</button>
))}
</div>
<div
ref={gridRef}
role="grid"
aria-label="Emojis"
tabIndex={0}
onKeyDown={handleKeyDown}
style={{
display: "grid",
gridTemplateColumns: "repeat(8, 1fr)",
gap: 2,
padding: "0.5rem",
maxHeight: 240,
overflowY: "auto",
}}
>
{visibleEmojis.map((emoji, i) => (
<div key={`${emoji}-${i}`} role="row">
<button
role="gridcell"
aria-label={emoji}
aria-selected={i === activeIndex}
onClick={() => onSelect?.(emoji)}
style={{
width: 36,
height: 36,
display: "flex",
alignItems: "center",
justifyContent: "center",
border: i === activeIndex ? "2px solid #1a73e8" : "none",
borderRadius: "0.375rem",
backgroundColor: i === activeIndex ? "#e8f0fe" : "transparent",
cursor: "pointer",
fontSize: "1.25rem",
padding: 0,
background: "none",
}}
>
{emoji}
</button>
</div>
))}
</div>
</div>
);
}
What CI checks
Quality contract- Bundled, minified and gzipped against its tier budget
- Audited by axe in the state previewed above
- Rendered through react-dom/server with no browser globals
- Scanned for network calls, dangerous sinks, and unguarded animation