Skip to article
API reference / JavaScript SDK

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 emojisense

Engine

createEngine

Builds the in-memory dictionary index from one or more packs: several languages, core and extension parts. Searching is synchronous.

TypeScript
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
}
Example
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" }, …]
  • limit defaults to 24. prefix (default true) treats the last word as a prefix while someone types. Pass false for complete queries.
  • locale prefers 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, now and day control the culture layer. Without a culture file they change nothing.

Result types

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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 optionNotes
endpointRequired. The API base URL.
keyPublishable key, sent as ?key= so there is no CORS preflight
packVersionPins the data so API results match your packs
cacheSizeIn-memory cache of recent answers. Default 200.
overLimitCooldownMsSkip the API for this long after an over-limit answer. Default 0: keep asking, because the edge cache may still know the next query.
fetchYour own fetch, for tests or proxies

shardKeyFor(keys, query) picks the shard file for a query, for custom shard hosting.

Fusion

  • shouldUseSemantic(alias): true when 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 from calibration.floor to calibration.ceiling (default DEFAULT_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 own k (60), pinScore (0.9), aliasFloor (off), aliasWeight and semanticWeight.

Helpers

TypeScript
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) and tokenize(normalized) follow the pack format’s normalization. MAX_QUERY_LENGTH is 64. embeddingText(input) is the lighter form the semantic tier embeds: accents, punctuation and emoji stay.
  • applySkinTone(emoji, tone) with a tone from SKIN_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_REACTIONS lists 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.

TypeScript
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 to createEngine(packs, { culture }) or engine.withCulture(culture); a search session applies it after fusion.
  • Culture results have source: "culture", context (the reason, in the file’s language) and cultureId. Search option culture: false keeps 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 a region in its scope, for a query equal to its trigger, and over a canonical answer it names. “football” with region: "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"), or undefined without a region subtag. regionOf(locale) does the same for any locale tag. useEmojisense and <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, insertCulture and applyCulture apply a culture file to your own result list; isActiveOn(when, day) and localDay(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.

TypeScript
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.