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/dompnpm add @emojisense/tiptap emojisense @tiptap/core @tiptap/pm @tiptap/suggestion @floating-ui/domyarn add @emojisense/tiptap emojisense @tiptap/core @tiptap/pm @tiptap/suggestion @floating-ui/dombun add @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",
}),
],
});import { useEmojisense } from "@emojisense/react";
import { EmojiAutocomplete } from "@emojisense/tiptap";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { useRef } from "react";
export function Composer() {
const sense = useEmojisense({
packBaseUrl: "https://api.emojisense.com/v1/pack/0.1.0",
endpoint: "https://api.emojisense.com",
});
const senseRef = useRef(sense);
senseRef.current = sense;
// The packs load after the editor exists, so pass getters, read on each keystroke.
const editor = useEditor({
extensions: [
StarterKit,
EmojiAutocomplete.configure({
engine: () => senseRef.current.engine,
semantic: () => senseRef.current.semantic,
}),
],
});
return <EditorContent editor={editor} />;
}Options
| Option | Default | Notes |
|---|---|---|
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 |
locale | first pack | Preferred locale for ranking and labels |
limit | 8 | Menu size |
debounceMs | 200 | Delay 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. |
minQueryLength | 1 | Characters after the trigger before the menu opens |
shortcodes | true | Replace a typed :name: when name is an exact shortcode or emoji name |
render | createEmojiMenu() | Menu renderer, with the contract of the suggestion utility’s render |
menuContainer | document.body | The element the menu mounts into, or a getter. See below. |
Keyboard
| Input | Result |
|---|---|
: and one or more characters | The menu opens with the dictionary results of this keystroke |
| ↑ ↓ | Move the active option (wraps) |
| Enter, Tab | Insert the active emoji as text, in place of :query |
| Escape | Close 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.
Mount the menu in your own frame
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.
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.
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.