Skip to article
Integrations / Tiptap

Tiptap

Type a colon in a Tiptap editor and get emoji ranked by meaning.

On this page

@emojisense/tiptap adds : emoji autocomplete to Tiptap 3. Type :jurassic and the menu shows 🦖 🦕 🦟. Type :fire: and it becomes 🔥. It is built on the official @tiptap/suggestion utility.

Install

npm install @emojisense/tiptap emojisense @tiptap/core @tiptap/pm @tiptap/suggestion @floating-ui/dom

@floating-ui/dom is a peer dependency of @tiptap/suggestion 3.28 and later. It positions the menu. In React, also add @tiptap/react and @emojisense/react.

Setup

import { EmojiAutocomplete } from "@emojisense/tiptap";
import "@emojisense/tiptap/styles.css"; // optional default look
import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import { createEngine, createSemanticClient, loadPacks } from "emojisense";

const packs = await loadPacks({ baseUrl: "https://api.emojisense.com/v1/pack/0.1.0" });
const engine = createEngine(packs);

new Editor({
  element: document.querySelector("#editor")!,
  extensions: [
    StarterKit,
    EmojiAutocomplete.configure({
      engine,
      // Optional: meaning results from the API, and a skin tone.
      semantic: createSemanticClient({ endpoint: "https://api.emojisense.com", key: "pk_live_…" }),
      skinTone: "medium",
    }),
  ],
});

Options

OptionDefaultNotes
engine—The engine, or a getter. The menu stays closed while it is undefined.
semantic—A semantic provider (for example createSemanticClient, or chainProviders(shards, api)), or a getter
localefirst packPreferred locale for ranking and labels
limit8Menu size
debounceMs200Delay before a semantic request
skinTone"none"A skin tone, or a getter that follows a user preference
char":"Trigger character. The menu opens after a space, a ( or a line start.
minQueryLength1Characters after the trigger before the menu opens
shortcodestrueReplace a typed :name: when name is an exact shortcode or emoji name
rendercreateEmojiMenu()Menu renderer, with the contract of the suggestion utility’s render
menuContainerdocument.bodyThe element the menu mounts into, or a getter. See below.

Keyboard

InputResult
: and one or more charactersThe menu opens with the dictionary results of this keystroke
↑ ↓Move the active option (wraps)
Enter, TabInsert the active emoji as text, in place of :query
EscapeClose the menu and keep the typed text, until the next word
:trex:, :+1:, :sweat_smile:Replaced by the emoji while you type

Meaning results arrive after debounceMs and merge into the open menu without moving confident hits. The default menu is a listbox named “Emoji suggestions”. Focus stays in the editor, which gets aria-activedescendant while the menu is open.

The extension has priority: 101, like Tiptap’s Mention. While the menu is open, it gets Enter, Tab and the arrow keys before list items and other keymaps with the default priority. The menu never scrolls the page.

By default the menu mounts on <body>. To keep it inside a dialog or a scroll panel, pass that element. If it does not exist yet when you create the editor, for example a React ref, pass a getter: it is read when the menu opens.

TSX
const frameRef = useRef<HTMLDivElement>(null);

const editor = useEditor({
  extensions: [
    StarterKit,
    EmojiAutocomplete.configure({ engine, menuContainer: () => frameRef.current }),
  ],
});

return (
  <div ref={frameRef} style={{ position: "relative" }}>
    <EditorContent editor={editor} />
  </div>
);

Give the frame a non-static position, such as relative, if it must clip the menu or set its stacking order.

Your own menu

render follows the @tiptap/suggestion contract. props.items are suggestions with { emoji, id, label, source } and the skin tone applied. Call props.command(item) to insert one. Late meaning results arrive as another onUpdate with the same query.

TypeScript
EmojiAutocomplete.configure({
  engine,
  render: () => ({
    onStart: (props) => myMenu.open(props.items, props.command, props.mount),
    onUpdate: (props) => myMenu.update(props.items),
    onExit: () => myMenu.close(),
    onKeyDown: ({ event }) => myMenu.handleKey(event),
  }),
});

To restyle the default menu instead, pass createEmojiMenu({ className, ariaLabel }) or override the CSS custom properties in styles.css.