JavaScript SDK
Every export of the emojisense package: engine, loader, sessions, providers and fusion.
On this page
The emojisense package is the engine every integration is built on. It has no dependencies and runs in browsers, Node, Deno, Bun, Workers and extensions. Everything below is exported from the package root.
npm install emojisensepnpm add emojisenseyarn add emojisensebun add emojisenseEngine
createEngine
Builds the in-memory dictionary index from one or more packs: several languages, core and extension parts. Searching is synchronous.
function createEngine(
input: Pack | Pack[],
options?: { minCoverage?: number; culture?: Culture },
): AliasEngine;
interface AliasEngine {
search(query: string, options?: AliasSearchOptions): AliasSearchOutput;
// culture: false gives the canonical ranking, typed as alias results only
search(query: string, options: AliasSearchOptions & { culture: false }): CanonicalSearchOutput;
get(id: string): EmojiEntry | undefined;
withCulture(culture: Culture | undefined): AliasEngine; // shares the index
readonly culture: Culture | undefined;
readonly entries: readonly EmojiEntry[];
readonly locales: readonly string[];
readonly packVersion: string;
}
interface AliasSearchOptions {
limit?: number; // default 24
locale?: string;
prefix?: boolean; // default true
culture?: boolean; // false = canonical ranking only
region?: string; // ISO 3166-1 alpha-2, for regional culture entries
now?: Date | number; // the day culture windows are checked against
day?: string; // "YYYY-MM-DD", wins over now
}const packs = await loadPacks({ baseUrl: "https://api.emojisense.com/v1/pack/0.1.0" });
const engine = createEngine(packs);
engine.search("greatest of all time").results;
// [{ emoji: "🐐", id: "1F410", score: 0.88, source: "alias" }, …]limitdefaults to 24.prefix(defaulttrue) treats the last word as a prefix while someone types. Passfalsefor complete queries.localeprefers phrases from that language’s pack when ranking.minCoverage(default 0.34) is the share of a multi-word query a phrase must cover.culture,region,nowanddaycontrol the culture layer. Without a culture file they change nothing.
Result types
type ResultSource = "alias" | "semantic" | "custom" | "culture";
interface SearchResult {
emoji: string; // "🦖", or ":shortcode:" for a custom emoji
id: string; // Emojibase hexcode of the base emoji, "1F996"; "C-<id>" for a custom emoji
score: number; // 0–1, comparable within one source only
source: ResultSource;
imageUrl?: string; // custom emoji: the image to draw
shortcode?: string; // custom emoji: the shortcode without colons
}
interface AliasResult extends SearchResult {
source: "alias" | "custom";
label: string; // the emoji's name
match: string; // the phrase that matched best
field: "name" | "shortcode" | "keyword" | "alias" | "typo" | "low";
}
interface CultureResult extends SearchResult {
source: "culture";
context: string; // why the emoji fits, in the culture file's language
cultureId: string; // e.g. "goat-football"
match: string; // the trigger that matched
field: "culture";
}
interface AliasSearchOutput {
query: string; // normalized
tokens: string[];
results: (AliasResult | CultureResult)[]; // culture results only with a culture file
confidence: number; // score of the best canonical result, 0 when nothing matched
}
interface CanonicalSearchOutput extends AliasSearchOutput {
results: AliasResult[];
}
interface EmojiEntry {
emoji: string;
id: string;
group: string;
version: number;
hasSkinTones: boolean;
labels: Record<string, string>; // per loaded locale
imageUrl?: string; // custom emoji only
shortcode?: string; // custom emoji only
}Loading packs
loadPacks
Fetches and validates the packs of one pack version. The files are immutable, so the browser’s HTTP cache does the rest.
function loadPacks(options: {
baseUrl: string; // "https://api.emojisense.com/v1/pack/0.1.0"
locales?: string[]; // default ["en"]; English always loads first
part?: "core" | "ext"; // default "core"
fetch?: typeof fetch;
signal?: AbortSignal;
}): Promise<Pack[]>;
// Your app's custom emoji as a pack, searched on the device next to the standard set.
function loadCustomPack(options: {
endpoint: string; // the API base URL
key: string; // publishable key
tenant?: string; // your customer's external id (tenants, Scale plan)
fetch?: typeof fetch;
signal?: AbortSignal;
}): Promise<Pack>;loadCustomPack fetches your custom emoji as a pack (part: "custom", checked with isCustomPack); pass it to createEngine with the other packs. Custom rows use ids that start with CUSTOM_ID_PREFIX, and their results carry imageUrl and shortcode.
assertPack(value) validates a pack you load yourself, for example from your app bundle. PACK_FORMAT, PACK_FORMAT_VERSION, FIELDS, DEFAULT_WEIGHTS and ROW_INDEX describe the format. See Pack format.
Search sessions
createSearchSession
The controller behind every picker: dictionary results on every keystroke, then debounced, cancellable meaning results fused in. Stale answers are dropped.
function createSearchSession(options: {
engine: AliasEngine;
semantic?: SemanticProvider; // omit for fully offline search
locale?: string;
limit?: number; // default 24
debounceMs?: number; // default 200
shouldUseSemantic?: (alias: AliasSearchOutput) => boolean;
culture?: Culture | false; // default: the engine's; false = canonical ranking only
region?: string; // for regional culture entries, e.g. "BR"
onChange: (state: SessionState) => void;
}): { update(query: string): void; dispose(): void };
interface SessionState {
query: string;
results: SearchResult[]; // with culture results after the top result
alias: CanonicalSearchOutput; // the dictionary answer, no culture
status: "idle" | "alias" | "loading" | "fused" | "error";
aliasMs: number;
semanticMs?: number;
semanticCached?: boolean;
layer?: "device" | "shard" | "api";
error?: unknown;
}Semantic providers
A provider answers a query or returns undefined, so the next provider gets it. With no provider left, the session keeps the dictionary results.
interface SemanticProvider {
// undefined means "no answer here": the next provider gets the query.
search(
query: string,
options?: { locale?: string; limit?: number; signal?: AbortSignal },
): Promise<SemanticResponse | undefined>;
}
// Layer 3, the API (GET /v1/search?mode=semantic)
createSemanticClient({ endpoint: "https://api.emojisense.com", key: "pk_live_…" });
// Layer 2, precomputed results as static files
createShardProvider({ baseUrl: "https://api.emojisense.com/p/0.1.0" });
// Both, cheapest first. Returns undefined when neither is configured.
createLayeredSemantic({ shardsUrl, endpoint, key, packVersion });
// Any providers, in order: the first answer wins.
chainProviders(shards, api);| createSemanticClient option | Notes |
|---|---|
endpoint | Required. The API base URL. |
key | Publishable key, sent as ?key= so there is no CORS preflight |
packVersion | Pins the data so API results match your packs |
cacheSize | In-memory cache of recent answers. Default 200. |
overLimitCooldownMs | Skip the API for this long after an over-limit answer. Default 0: keep asking, because the edge cache may still know the next query. |
fetch | Your own fetch, for tests or proxies |
shardKeyFor(keys, query) picks the shard file for a query, for custom shard hosting.
Fusion
shouldUseSemantic(alias):truewhen the confidence is under 0.6, or for a phrase of two or more words under 0.9.fuse(alias, semantic, limit, calibration): merges with weights from the confidence of each tier. The dictionary weight is 0.4 + its confidence. The semantic weight goes from 0.4 to 1 as its best cosine goes fromcalibration.floortocalibration.ceiling(defaultDEFAULT_SEMANTIC_CALIBRATION, 0.44–0.58, for bge-m3). Results with a score of 0.9 or more stay pinned on top. When the dictionary is sure (confidence 0.6 or more), its results within 0.1 of its top score come next, in fused order. Semantic country flags that the dictionary results do not hold go last, unless their cosine reaches the calibration ceiling.semanticConfidence(semantic, calibration): that 0–1 value. Use your own calibration if you embed with another model.fuseResults(alias, semantic, options): the same, with your ownk(60),pinScore(0.9),aliasFloor(off),aliasWeightandsemanticWeight.
Helpers
normalize("¡Feliz cumpleaños!"); // "feliz cumpleanos"
tokenize(normalize("Ship it!")); // ["ship","it"]
applySkinTone("👍", "medium-dark"); // "👍🏾"
baseId("1F44D-1F3FD"); // "1F44D"
groupLabel("food-drink", "tr"); // "Yiyecek ve içecek"
hexcodeOf("❤️🔥"); // "2764-FE0F-200D-1F525"normalize(input, maxLength)andtokenize(normalized)follow the pack format’s normalization.MAX_QUERY_LENGTHis 64.embeddingText(input)is the lighter form the semantic tier embeds: accents, punctuation and emoji stay.applySkinTone(emoji, tone)with a tone fromSKIN_TONES.baseId(hexcode)maps a skin-tone variant to its base emoji.groupLabel(group, locale)names a pack group for the UI.hexcodeOf(emoji)gives the Emojibase hexcode of an emoji, skin tone included.boundedEditDistance(a, b, max)is the typo distance the engine uses.COMMON_REACTIONSlists the emoji people react with most (the defaults of Slack, GitHub and Discord), for a reaction bar.
Culture layer
Editorial emoji for a culture, a region and a moment (football’s greatest-of-all-time debate, Día de Muertos, Diwali), from one small file per locale at /v1/culture/<packVersion>/culture.<locale>.json. Editors, helped by AI, write and approve them. They join the results right after the top answer and never replace it.
const culture = await loadCulture({ baseUrl: "https://api.emojisense.com/v1/culture/0.1.0", locale: "es" });
const cultured = engine.withCulture(culture); // shares the index
cultured.search("goat", { locale: "es" }).results;
// [🐐 (alias), ⚽ 🇦🇷 🇵🇹 (source "culture", context, cultureId), …]
cultured.search("goat", { culture: false }); // the canonical ranking only
relevantNow(culture, { region: "MX", limit: 8 });
// [{ emoji: "💀", hexcode: "1F480", context: "Día de Muertos, 1 y 2 de noviembre", cultureId: "dia-de-muertos" }, …]loadCulture({ baseUrl, locale })fetches and checks a culture file (assertCulture,CULTURE_FORMAT,CULTURE_FORMAT_VERSION). Pass it tocreateEngine(packs, { culture })orengine.withCulture(culture); a search session applies it after fusion.- Culture results have
source: "culture",context(the reason, in the file’s language) andcultureId. Search optionculture: falsekeeps the canonical ranking;region(for example"BR") turns on regional entries. - A regional sense (
kind: "regional") is the one entry that can go first: only with aregionin its scope, for a query equal to its trigger, and over a canonical answer it names. “football” withregion: "GB"gives ⚽ then 🏈; without a region 🏈 stays first.matchRegionalLead(culture, query, topId, { region })applies that rule to your own list. deviceRegion()returns the region of the browser’s language (navigator.language"pt-BR"→"BR"), orundefinedwithout a region subtag.regionOf(locale)does the same for any locale tag.useEmojisenseand<emojisense-picker>use the browser’s region when the app passes none. The region stays on the device: it only selects regional entries and is never sent.relevantNow(culture, options)lists featured seasonal and event emoji active today, for an optional shelf.matchCulture,insertCultureandapplyCultureapply a culture file to your own result list;isActiveOn(when, day)andlocalDay(now)evaluate windows. See Pack format §9.
Emoji sets
For hosted emoji sets: EMOJI_SETS, isEmojiSet(value) and emojiImageUrl(emoji, { endpoint, emojiSet }), which returns undefined for native or without an endpoint.
EMOJI_SETS; // ["native", "twemoji", "noto", "fluent"]
isEmojiSet("noto"); // true
emojiImageUrl("🦖", { endpoint: "https://api.emojisense.com", emojiSet: "noto" });
// "https://api.emojisense.com/v1/sets/noto/1F996.svg"Vectors
For on-device meaning search experiments and for building vector files: decodeVectors(buffer), encodeVectors(model, ids, rows), l2normalize(vector) and searchVectors(index, query, k). searchVectorSets(indexes, query, k) searches several vector files at once (for example the shared English file and one locale file) and keeps each emoji's best row. The file format is in Pack format §5.