DocsBrowse documentation
Search & Metadata
Most of this page assumes app-owned generated artifacts built with @slithy/emoji-transforms, then passed into the picker as plain maps.
Grouped vs. Unified search
Supplemental search defaults to grouped results. Set mode: "unified" to merge native and supplemental matches into a single results surface.
In grouped mode, native matches stay under their native category headings and supplemental matches stay under the labels of the matching sections.
Use unified mode when you want search to act like one merged emoji vocabulary:
- native and supplemental matches flatten into one results section
- ranking becomes the primary organizing signal
resultsLabelnames that merged results surface
<EmojiPicker.Root
supplemental={{
sections: [supplementalSection],
search: {
mode: "unified",
resultsLabel: "Results",
},
}}
>
...
</EmojiPicker.Root>Runtime vs. Pre-built data
@slithy/frimousse already fetches native emoji data at runtime by default. That is a good fit for the picker dataset itself, especially when you want to stay close to the default setup or point the picker at self-hosted Emojibase files with emojibaseUrl.
The metadata maps on this page are usually a better fit for pre-built app-owned artifacts than for runtime transformation in the browser.
Good runtime inputs
- the native emoji dataset the picker loads
- self-hosted Emojibase JSON served to the picker
- simple prototypes or internal tools
- quick experiments with one locale
Runtime example: workspace emoji
Workspace-specific custom emoji is a good runtime input because it is dynamic, tenant-specific, and naturally fetched per session.
import { useEffect, useState } from "react";
import {
EmojiPicker,
createSupplementalSection,
type EmojiPickerSection,
} from "@slithy/frimousse";
type WorkspaceEmojiResponse = {
items: Array<{
id: string;
label: string;
imageUrl: string;
aliases?: string[];
}>;
};
export function WorkspaceEmojiPicker() {
const [sections, setSections] = useState<EmojiPickerSection[]>([]);
useEffect(() => {
let cancelled = false;
async function loadWorkspaceEmoji() {
const response = await fetch("/api/workspaces/acme/emoji");
const data = (await response.json()) as WorkspaceEmojiResponse;
if (cancelled) {
return;
}
setSections([
createSupplementalSection(data.items, {
id: "workspace-emoji",
label: "Workspace emoji",
position: "append",
searchable: true,
}),
]);
}
loadWorkspaceEmoji();
return () => {
cancelled = true;
};
}, []);
return (
<EmojiPicker.Root supplemental={{ sections }}>
...
</EmojiPicker.Root>
);
}This is a strong runtime case because the section is consumer-owned, changes over time, and is specific to the current workspace or session. Unlike search-term maps or label maps, this data is not just a static metadata transform.
Better as pre-built artifacts
- native search term maps
- shortcode maps
- label maps
- English fallback maps
- multilanguage search or label maps
Pre-building keeps search policy, localization policy, and metadata ownership in your app instead of in the picker runtime. It also avoids shipping full source datasets to the browser when the picker only needs the final plain-object maps.
It also improves the performance profile for production apps: pre-built maps reduce client-side parsing and transformation work, keep browser bundles smaller, and avoid recomputing the same metadata on every load. Runtime transforms are usually acceptable when the datasets are small or the app is lightly used, but they become a less attractive default once you care about startup cost, bundle control, or repeated locale-specific processing.
Runtime transforms are still reasonable for prototypes, internal tools, or tightly controlled environments. For production apps, prefer generating the final search, label, shortcode, and fallback maps ahead of time and passing those results into the picker.
Build-time generation pattern
Once you decide a map should be pre-built, the usual pattern is:
- read the source emoji dataset during your app build or data-prep step
- generate the plain-object maps your app needs
- write those artifacts into a generated file your client code can import
// scripts/generate-emoji-maps.ts
import fs from "node:fs/promises";
import emojiData from "emojibase-data/en/data.json";
import iamcalShortcodes from "emojibase-data/en/shortcodes/iamcal.json";
import {
buildLabelMapFromEmojibase,
buildNativeEmojiSearchTermMapFromEmojibase,
buildShortcodeMapFromPreset,
} from "@slithy/emoji-transforms";
const nativeSearchTerms = buildNativeEmojiSearchTermMapFromEmojibase(
emojiData,
{ includeTags: true },
);
const nativeLabels = buildLabelMapFromEmojibase(emojiData);
const nativeShortcodes = buildShortcodeMapFromPreset(
emojiData,
iamcalShortcodes,
);
await fs.writeFile(
"./src/generated/emoji-maps.ts",
[
"export const nativeSearchTerms = " +
JSON.stringify(nativeSearchTerms, null, 2),
"export const nativeLabels = " +
JSON.stringify(nativeLabels, null, 2),
"export const nativeShortcodes = " +
JSON.stringify(nativeShortcodes, null, 2),
].join("\n\n"),
);pnpm exec tsx scripts/generate-emoji-maps.tsThen import only the generated artifacts in app code:
import {
nativeLabels,
nativeSearchTerms,
nativeShortcodes,
} from "./generated/emoji-maps";The sections below apply this same pattern to different kinds of metadata. What changes is the map you generate, not the overall build-step workflow.
Native search enrichment
Use native search enrichment when the default native dataset search is not enough for your app.
search.native.terms adds app-owned vocabulary to the pickerβs built-in native search. This is the right place for richer aliases, domain-specific language, localized synonyms, or shortcode-style terms that should match native emoji.
Following the build-time pattern above, generate the final plain-object map ahead of time, then pass that artifact to EmojiPicker.Root.
import emojiData from "emojibase-data/en/data.json";
import {
buildNativeEmojiSearchTermMapFromEmojibase,
mergeNativeEmojiSearchTermMaps,
} from "@slithy/emoji-transforms";
const generatedNativeSearchTerms = buildNativeEmojiSearchTermMapFromEmojibase(
emojiData,
{ includeLabel: true, includeTags: true },
);
const nativeSearchTerms = mergeNativeEmojiSearchTermMaps(
generatedNativeSearchTerms,
{
"π": ["catch you later"],
"β€οΈ": ["favorite"],
},
);import { EmojiPicker } from "@slithy/frimousse";
<EmojiPicker.Root
search={{
native: {
terms: nativeSearchTerms,
},
}}
>
<EmojiPicker.Search />
<EmojiPicker.Viewport>
<EmojiPicker.List />
</EmojiPicker.Viewport>
</EmojiPicker.Root>;The generated map looks like this:
{
"π": ["good_bye", "waving hand", "see you", "wave", "catch you later"],
"π": ["hyper link", "url"],
"β€οΈ": ["red heart", "love", "favorite"],
}This enriches the default native search rather than replacing it. The picker still matches its built-in native metadata, and search.native.terms adds the extra terms your app wants to support. In this example, shortcode-like terms such as "good_bye" come from shortcode data, label-like terms such as "waving hand" or "red heart" come from includeLabel: true, broader discovery terms such as "see you" or "love" come from includeTags: true, and custom app-owned terms such as "catch you later" or "favorite" are layered in afterward.
Shortcode search
Use shortcode search when your app has a preferred native shortcode vocabulary and wants those terms to participate in native emoji search.
Following the build-time pattern above, generate the shortcode map from your preferred preset, then pass that map into search.native.terms.
import emojiData from "emojibase-data/en/data.json";
import iamcalShortcodes from "emojibase-data/en/shortcodes/iamcal.json";
import { buildShortcodeMapFromPreset } from "@slithy/emoji-transforms";
const nativeSearchTerms = buildShortcodeMapFromPreset(
emojiData,
iamcalShortcodes,
);import { EmojiPicker } from "@slithy/frimousse";
<EmojiPicker.Root
search={{
native: {
terms: nativeSearchTerms,
},
}}
>
...
</EmojiPicker.Root>;The generated map looks like this:
{
"π": ["wave", "good_bye"],
"π": ["link"],
"β€οΈ": ["red_heart"],
}Use a preset-driven shortcode map when your app treats a vocabulary such as iamcal as the source of truth for native shortcode search, display, or persistence.
English fallback search
If your picker renders in a non-English locale but should still match common English queries, merge the active locale terms with fallback terms from an English dataset.
Use the build-time generation pattern above when you want to materialize these merged fallback terms as an app-owned artifact.
import englishData from "emojibase-data/en/data.json";
import frenchData from "emojibase-data/fr/data.json";
import {
buildFallbackTermsFromEmojibase,
buildNativeEmojiSearchTermMapFromEmojibase,
mergeNativeEmojiSearchTermMaps,
} from "@slithy/emoji-transforms";
const nativeSearchTerms = mergeNativeEmojiSearchTermMaps(
buildNativeEmojiSearchTermMapFromEmojibase(frenchData, {
includeLabel: true,
}),
buildFallbackTermsFromEmojibase(englishData),
);import { EmojiPicker } from "@slithy/frimousse";
<EmojiPicker.Root
locale="fr"
search={{
native: {
terms: nativeSearchTerms,
},
}}
>
...
</EmojiPicker.Root>;This is a good fit when the picker should primarily reflect one locale but still recognize common English queries. The active locale terms stay first, and the English map broadens recall without changing the pickerβs locale.
Multilanguage maps
Build plain label maps per locale, then merge them in precedence order.
Use the build-time generation pattern above when you want to write these merged locale maps into generated files for app code to import directly.
import englishData from "emojibase-data/en/data.json";
import simplifiedChineseData from "emojibase-data/zh/data.json";
import traditionalChineseData from "emojibase-data/zh-hant/data.json";
import {
buildLabelMapFromEmojibase,
getLabel,
mergeNativeEmojiLabelMaps,
} from "@slithy/emoji-transforms";
const labels = mergeNativeEmojiLabelMaps(
buildLabelMapFromEmojibase(traditionalChineseData),
buildLabelMapFromEmojibase(simplifiedChineseData),
buildLabelMapFromEmojibase(englishData),
);
getLabel(labels, "β€οΈ");Earlier maps win, so pass your preferred locale first and fallbacks after it.
Use the same merge pattern for search-term maps when your app needs multilingual search precedence, but keep label and search artifacts separate so each map stays scoped to one job.