Emoji Picker
Accessible, unstyled, data-free emoji picker: consumers supply typed emoji data.
|
|
| Package |
@vizejs/ui/emoji-picker |
| Maturity |
stable |
| Own the source |
vize lib pull emoji-picker |
| Requires |
context, controllable-state, id, listbox-grid |
| Aliases |
emoji picker, emoji selector, reaction picker |
| Covers |
WAI-ARIA grid pattern, emoji-mart, Frimousse, Ark UI |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { EmojiPicker, EmojiPickerRoot, EmojiPickerCategory, EmojiPickerEmpty, EmojiPickerGrid, EmojiPickerItem, EmojiPickerPreview, EmojiPickerSearch, EmojiPickerSkinTone } from "@vizejs/ui/emoji-picker";
Or copy the source into your project with vize lib pull emoji-picker (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Emoji picker over consumer-supplied emoji data with search, categories, skin tones, and a preview. -->
<script setup lang="ts">
import { ref } from "vue";
import {
EmojiPicker,
EmojiPickerCategory,
EmojiPickerEmpty,
EmojiPickerGrid,
EmojiPickerItem,
EmojiPickerPreview,
EmojiPickerSearch,
EmojiPickerSkinTone,
} from "@vizejs/ui/emoji-picker";
interface Emoji {
readonly emoji: string;
readonly name: string;
readonly category: "smileys" | "animals";
readonly keywords: readonly string[];
readonly skins?: readonly string[];
}
const emoji: readonly Emoji[] = [
{ emoji: "😀", name: "grinning face", category: "smileys", keywords: ["happy"] },
{ emoji: "😂", name: "tears of joy", category: "smileys", keywords: ["laugh"] },
{
emoji: "👋",
name: "waving hand",
category: "smileys",
keywords: ["hello", "bye"],
skins: ["👋", "👋🏻", "👋🏼", "👋🏽", "👋🏾", "👋🏿"],
},
{ emoji: "🐱", name: "cat face", category: "animals", keywords: ["pet"] },
{ emoji: "🐶", name: "dog face", category: "animals", keywords: ["puppy", "pet"] },
];
const categories = [
{ id: "smileys", label: "Smileys & people" },
{ id: "animals", label: "Animals" },
];
const picked = ref("");
</script>
<template>
<div>
<EmojiPicker
v-slot="{ sections }"
:items="emoji"
:categories
:columns="4"
:get-emoji="(item: Emoji) => item.emoji"
:get-name="(item: Emoji) => item.name"
:get-category="(item: Emoji) => item.category"
:get-keywords="(item: Emoji) => item.keywords"
:get-skins="(item: Emoji) => item.skins"
@select="(_item: Emoji, glyph: string) => (picked = glyph)"
>
<EmojiPickerSearch placeholder="Search emoji" />
<EmojiPickerSkinTone />
<EmojiPickerGrid>
<EmojiPickerCategory
v-for="section in sections"
:key="section.id"
v-slot="{ item, index }"
:section
>
<EmojiPickerItem :item :index />
</EmojiPickerCategory>
</EmojiPickerGrid>
<EmojiPickerEmpty>No emoji found</EmojiPickerEmpty>
<EmojiPickerPreview />
</EmojiPicker>
<output>Picked: {{ picked || "none" }}</output>
</div>
</template>
API
EmojiPicker<T>
Source: src/families/selection/emoji-picker/emoji-picker-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
items (required) |
readonly T[] |
required |
Every emoji item, in display order. No dataset ships with the picker. |
getEmoji (required) |
(item: T) => string |
required |
Native glyph for the default tone. |
getName (required) |
(item: T) => string |
required |
Human-readable name used for search, labels, and the preview. |
getKeywords |
(item: T) => readonly string[] |
undefined |
Extra search keywords. |
getCategory |
(item: T) => string |
undefined |
Category id of an item; omit for a single uncategorized section. |
getSkins |
(item: T) => readonly string[] | undefined |
undefined |
Skin-tone variants indexed 0–5 (0 is the default tone). |
categories |
readonly EmojiPickerCategory[] |
undefined |
Category order and labels; unknown categories follow in first-seen order. |
columns |
number |
8 |
Cells per row; must match the consumer's CSS grid. |
skinTone |
EmojiSkinTone |
undefined |
Controlled skin tone. undefined selects uncontrolled behavior. |
defaultSkinTone |
EmojiSkinTone |
0 |
Initial skin tone for uncontrolled use. |
search |
string |
undefined |
Controlled search text. undefined selects uncontrolled behavior. |
filter |
EmojiPickerFilter<T> |
undefined |
Search filter. Defaults to an accent- and case-insensitive name/keyword match. |
recent |
readonly T[] |
undefined |
Consumer-owned recently used items, shown as the first section. |
recentLabel |
string |
"Recently used" |
Label of the recent section. |
searchLabel |
string |
"Search results" |
Label of the search results section. |
defaultLabel |
string |
"Emoji" |
Label of the single section used when getCategory is omitted. |
dir |
"ltr" | "rtl" |
"ltr" |
Reading direction; rtl mirrors ArrowLeft and ArrowRight. |
Events
| Event |
Payload |
Description |
select |
[item: T, glyph: string] |
Fired when an emoji is chosen, with the item and its glyph with the skin tone applied. |
update:skinTone |
[tone: EmojiSkinTone] |
Fired when the skin tone requests a new controlled value. |
update:search |
[text: string] |
Fired when the search text requests a new controlled value. |
Slots
| Slot |
Slot props |
Description |
default |
EmojiPickerSlotState<T> |
Search, grid, categories, skin tones, and preview. Receives typed sections. |
Exposed
| Member |
Type |
Description |
activeItem |
— |
|
focusGrid |
— |
|
setSearch |
— |
|
skinTone |
— |
|
EmojiPickerRoot<T>
Source: src/families/selection/emoji-picker/emoji-picker-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
items (required) |
readonly T[] |
required |
Every emoji item, in display order. No dataset ships with the picker. |
getEmoji (required) |
(item: T) => string |
required |
Native glyph for the default tone. |
getName (required) |
(item: T) => string |
required |
Human-readable name used for search, labels, and the preview. |
getKeywords |
(item: T) => readonly string[] |
undefined |
Extra search keywords. |
getCategory |
(item: T) => string |
undefined |
Category id of an item; omit for a single uncategorized section. |
getSkins |
(item: T) => readonly string[] | undefined |
undefined |
Skin-tone variants indexed 0–5 (0 is the default tone). |
categories |
readonly EmojiPickerCategory[] |
undefined |
Category order and labels; unknown categories follow in first-seen order. |
columns |
number |
8 |
Cells per row; must match the consumer's CSS grid. |
skinTone |
EmojiSkinTone |
undefined |
Controlled skin tone. undefined selects uncontrolled behavior. |
defaultSkinTone |
EmojiSkinTone |
0 |
Initial skin tone for uncontrolled use. |
search |
string |
undefined |
Controlled search text. undefined selects uncontrolled behavior. |
filter |
EmojiPickerFilter<T> |
undefined |
Search filter. Defaults to an accent- and case-insensitive name/keyword match. |
recent |
readonly T[] |
undefined |
Consumer-owned recently used items, shown as the first section. |
recentLabel |
string |
"Recently used" |
Label of the recent section. |
searchLabel |
string |
"Search results" |
Label of the search results section. |
defaultLabel |
string |
"Emoji" |
Label of the single section used when getCategory is omitted. |
dir |
"ltr" | "rtl" |
"ltr" |
Reading direction; rtl mirrors ArrowLeft and ArrowRight. |
Events
| Event |
Payload |
Description |
select |
[item: T, glyph: string] |
Fired when an emoji is chosen, with the item and its glyph with the skin tone applied. |
update:skinTone |
[tone: EmojiSkinTone] |
Fired when the skin tone requests a new controlled value. |
update:search |
[text: string] |
Fired when the search text requests a new controlled value. |
Slots
| Slot |
Slot props |
Description |
default |
EmojiPickerSlotState<T> |
Search, grid, categories, skin tones, and preview. Receives typed sections. |
Exposed
| Member |
Type |
Description |
activeItem |
T | undefined |
Highlighted item. |
skinTone |
EmojiSkinTone |
Current skin tone. |
focusGrid |
() => void |
Move DOM focus into the grid. |
setSearch |
(text: string) => void |
Replace the search text. |
EmojiPickerCategory<T>
Source: src/families/selection/emoji-picker/emoji-picker-category.vue
Props
| Prop |
Type |
Default |
Description |
section (required) |
EmojiPickerSection<T> |
required |
Section from the root slot state sections. |
Slots
| Slot |
Slot props |
Description |
default |
EmojiPickerCellSlotState<T> |
Render one EmojiPickerItem per cell with :item and :index. |
label |
{ readonly label: string } |
Custom section label content. |
EmojiPickerEmpty
Source: src/families/selection/emoji-picker/emoji-picker-empty.vue
Slots
| Slot |
Slot props |
Description |
default |
{ readonly search: string } |
Message shown while no emoji is visible. Receives the search text. |
EmojiPickerGrid
Source: src/families/selection/emoji-picker/emoji-picker-grid.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
"Emoji" |
Accessible name of the grid. |
Slots
| Slot |
Slot props |
Description |
default |
— |
One EmojiPickerCategory per section from the root slot state. |
EmojiPickerItem<T>
Source: src/families/selection/emoji-picker/emoji-picker-item.vue
Props
| Prop |
Type |
Default |
Description |
item (required) |
T |
required |
Emoji item rendered by this cell. |
index (required) |
number |
required |
Item index inside its section, from the category slot. |
Slots
| Slot |
Slot props |
Description |
default |
EmojiPickerItemSlotState<T> |
Cell content; defaults to the glyph. Receives glyph, name, and highlight state. |
EmojiPickerPreview
Source: src/families/selection/emoji-picker/emoji-picker-preview.vue
Slots
| Slot |
Slot props |
Description |
default |
EmojiPickerPreviewSlotState |
Preview of the highlighted emoji. Defaults to its glyph and name. |
EmojiPickerSearch
Source: src/families/selection/emoji-picker/emoji-picker-search.vue
Props
| Prop |
Type |
Default |
Description |
placeholder |
string |
undefined |
Hint text shown while empty. |
ariaLabel |
string |
"Search emoji" |
Accessible name of the search field. |
EmojiPickerSkinTone
Source: src/families/selection/emoji-picker/emoji-picker-skin-tone.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
"Skin tone" |
Accessible name of the radio group. |
labels |
readonly string[] |
["Default", "Light", "Medium-light", "Medium", "Medium-dark", "Dark"] |
Accessible names of tones 0–5. |
Slots
| Slot |
Slot props |
Description |
default |
{ readonly tone: EmojiSkinTone; readonly label: string; readonly checked: boolean; } |
Content of one tone swatch. Receives the tone, its label, and checked state. |
Behavior
Contract
EmojiPicker is a headless, data-free emoji picker: no dataset ships, and the
consumer maps typed items through required getEmoji/getName accessors plus
optional getKeywords, getCategory, and getSkins. emoji-picker-root.vue
is generic over the item type T, builds sections (recent first, then
categories in order, then unknown categories; a non-empty search collapses
into one "search" section), owns the controllable skinTone and search, and
emits select(item, glyph) with the tone applied.
emoji-picker-grid.vue renders the APG role="grid" focus owner with
aria-activedescendant. Navigation runs over the concatenated rows of every
visible section: each section starts a new row and short rows are padded, so
the listbox-grid moveInGrid helper moves in two dimensions and skips padding.
emoji-picker-category.vue renders a labelled role="rowgroup" with a
columnheader label row and role="row" chunks, emoji-picker-item.vue a
role="gridcell", emoji-picker-search.vue a search field,
emoji-picker-skin-tone.vue a roving radiogroup of six tones,
emoji-picker-preview.vue a polite live preview, and emoji-picker-empty.vue
the no-results state. No CSS ships.
Normative Behavior
| # |
State |
Input |
Outcome |
Proven by |
| E1 |
any |
render |
focusable grid with aria-colcount/aria-rowcount (item plus label rows), labelled rowgroups, named gridcells |
renders an APG grid with labelled category rowgroups and gridcells |
| E2 |
grid focused |
arrows / Home / End / Ctrl+Home/End / PageUp/Down |
focus highlights the first cell; moves in 2-D across section boundaries, skipping padding cells |
2-D keyboard navigation crosses section boundaries over padded rows |
| E3 |
highlighted cell |
Enter / Space / click |
emits select(item, glyph) with the skin tone applied; clicked cells become active |
Enter, Space, and click emit select with the skin tone applied |
| E4 |
skin tones |
arrows / Home / End / click |
roving tabindex radiogroup; arrows wrap and select; glyphs update; update:skinTone emits |
the skin-tone radiogroup uses roving focus and updates glyphs |
| E5 |
controlled tone |
click a tone |
emits and waits for the parent |
controlled skin tone waits for the parent |
| E6 |
search |
type / Enter / Escape / ArrowDown |
filters names and keywords accent-insensitively into one section, highlights the first match, Enter selects it, empty state shows, Escape clears, ArrowDown enters the grid |
search filters by name and keyword, highlights the first match, and drives the empty state |
| E7 |
recent |
render / navigate |
recent items form the first section; repeated items keep unique cell ids |
recent items form the first section and repeat safely |
| E8 |
pointer |
pointer move |
highlights the cell and the live preview shows glyph and name |
the preview follows the highlighted emoji |
| E9 |
no provider |
mount a part |
throws VIZE_UI_CONTEXT_MISSING |
parts require an EmojiPicker provider |
| E10 |
pure helpers |
sections / glyphs / virtual grid |
unknown categories follow in first-seen order; padding cells are empty; tones clamp to 0–5 |
pure helpers build sections, glyphs, and the padded virtual grid |
SSR
Cell ids derive from the root id plus section and item positions, so server
markup is byte-identical across isolated requests and hydrates without warnings
(renders byte-identical EmojiPicker markup across isolated SSR requests,
hydrates EmojiPicker without mismatches or node replacement). Large sets are
filtered in O(n) per keystroke; rows are not virtualized.