Emoji
The Emoji extension adds inline emoji nodes with :shortcode: input rules, optional emoticon shortcuts (:), <3), a headless suggestion plugin for autocomplete dropdowns, and built-in frequency tracking. The extension ships as a separate package (@domternal/extension-emoji).
When to use
Section titled “When to use”Choose Emoji when you need:
- A
:shortcode:input rule that converts to a native emoji character - An autocomplete dropdown that opens on
:and suggests matching emoji - A picker UI over the built-in dataset (218 popular emoji, or 516 with
allEmojis) or your ownEmojiItem[] - Frequency-based ranking so the most-used emoji surface first
Skip it if:
- Your users only insert emoji from their OS keyboard and you do not need shortcodes
- You need custom organization emoji (build a
Mention-style extension with a custom data source) - Bundle size is critical (the default dataset adds about 5 KB gzipped,
allEmojisabout 11 KB)
Live Playground
Section titled “Live Playground”Type : followed by a name to open the suggestion dropdown, or use shortcodes like :smile: to insert emoji directly.
With the default theme enabled. Click the smiley toolbar button to trigger the suggestion dropdown, or type : followed by a name.
Plain emoji without the theme. The buttons above the editor call commands like insertEmoji() and suggestEmoji() directly. Emoticons (:), <3) are enabled. Type : to see inline suggestions.
Installation
Section titled “Installation”pnpm add @domternal/extension-emojiimport { Document, Paragraph, Text } from '@domternal/core';import { Emoji, emojis, createEmojiSuggestionRenderer } from '@domternal/extension-emoji';import { DomternalEditor } from '@domternal/vanilla';import '@domternal/theme';
const dm = new DomternalEditor(document.getElementById('editor')!, { extensions: [ Document, Paragraph, Text, Emoji.configure({ emojis, suggestion: { render: createEmojiSuggestionRenderer() }, }), ], content: '<p>Type :smile: to insert an emoji!</p>',});
// Handle the toolbar emoji button - open inline suggestiondm.editor.on('insertEmoji', () => { dm.editor.commands.suggestEmoji();});Type : followed by a shortcode (e.g. :rocket:) to see the inline suggestion dropdown. The toolbar emoji button emits an insertEmoji event - wire it to suggestEmoji() or build a custom picker panel.
import { Component, signal } from '@angular/core';import { DomternalEditorComponent, DomternalToolbarComponent, DomternalEmojiPickerComponent,} from '@domternal/angular';import { Editor, Document, Paragraph, Text } from '@domternal/core';import { Emoji, emojis, createEmojiSuggestionRenderer } from '@domternal/extension-emoji';
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent, DomternalToolbarComponent, DomternalEmojiPickerComponent], templateUrl: './editor.html',})export class EditorComponent { editor = signal<Editor | null>(null); emojiData = emojis; extensions = [ Document, Paragraph, Text, Emoji.configure({ emojis, suggestion: { render: createEmojiSuggestionRenderer() }, }), ]; content = '<p>Type :smile: to insert an emoji!</p>';}@if (editor(); as ed) { <domternal-toolbar [editor]="ed" />}<domternal-editor [extensions]="extensions" [content]="content" (editorCreated)="editor.set($event)"/>@if (editor(); as ed) { <domternal-emoji-picker [editor]="ed" [emojis]="emojiData" />}The <domternal-emoji-picker> automatically listens for the toolbar emoji button click and opens a full picker with search, categories, and frequently used.
import { Domternal } from '@domternal/react';import { Document, Paragraph, Text } from '@domternal/core';import { Emoji, emojis, createEmojiSuggestionRenderer } from '@domternal/extension-emoji';
export default function Editor() { return ( <Domternal extensions={[ Document, Paragraph, Text, Emoji.configure({ emojis, suggestion: { render: createEmojiSuggestionRenderer() }, }), ]} content="<p>Type :smile: to insert an emoji!</p>" > <Domternal.Toolbar /> <Domternal.Content /> <Domternal.EmojiPicker emojis={emojis} /> </Domternal> );}The <Domternal.EmojiPicker> automatically listens for the toolbar emoji button click and opens a full picker with search, categories, and frequently used.
<script setup lang="ts">import { Domternal } from '@domternal/vue';import { Document, Paragraph, Text } from '@domternal/core';import { Emoji, emojis, createEmojiSuggestionRenderer } from '@domternal/extension-emoji';
const extensions = [ Document, Paragraph, Text, Emoji.configure({ emojis, suggestion: { render: createEmojiSuggestionRenderer() }, }),];</script>
<template> <Domternal :extensions="extensions" content="<p>Type :smile: to insert an emoji!</p>"> <Domternal.Toolbar /> <Domternal.Content /> <Domternal.EmojiPicker :emojis="emojis" /> </Domternal></template>The <Domternal.EmojiPicker> automatically listens for the toolbar emoji button click and opens a full picker with search, categories, and frequently used.
import { Editor, Document, Paragraph, Text } from '@domternal/core';import { Emoji, emojis, createEmojiSuggestionRenderer } from '@domternal/extension-emoji';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [ Document, Paragraph, Text, Emoji.configure({ emojis, suggestion: { render: createEmojiSuggestionRenderer() }, }), ],});
// Insert an emoji by nameeditor.commands.insertEmoji('rocket');editor.commands.insertEmoji('red_heart');
// Open the inline suggestion dropdown programmaticallyeditor.commands.suggestEmoji();Schema
Section titled “Schema”| Property | Value |
|---|---|
| ProseMirror name | emoji |
| Type | Node |
| Group | inline |
| Content | None (atom node) |
| Atom | Yes |
| Inline | Yes |
| Selectable | Yes |
| Draggable | No |
| HTML tag | <span> (parses span[data-type="emoji"] and span[data-emoji-name]) |
Emoji is an inline atom node. It cannot contain text or other nodes. Click an emoji to select it (creates a NodeSelection), then press Backspace to delete or type to replace.
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
emojis | EmojiItem[] | Built-in ~218 emoji | Emoji dataset used for shortcode resolution, search, and rendering |
enableEmoticons | boolean | false | Enable emoticon shortcuts like :) and <3 (34 emoticons) |
plainText | boolean | false | Insert emoji as plain Unicode text instead of atom nodes |
toolbar | boolean | true | Show the emoji button in the main toolbar. Set to false to hide it, e.g. for Notion-style setups where emoji is only inserted via :shortcode: and the suggestion popup. NEW in v0.7.0. |
HTMLAttributes | Record<string, unknown> | {} | HTML attributes added to the emoji <span> element |
suggestion | SuggestionOptions | null | null | Suggestion plugin config for autocomplete. Pass null to disable |
Emoji datasets
Section titled “Emoji datasets”Two datasets are included:
| Dataset | Size | Description |
|---|---|---|
emojis | 218 emoji | Popular emoji across all categories (~5 KB gzipped) |
allEmojis | 516 emoji | Extended Unicode set (~11 KB gzipped) |
import { Emoji, emojis, allEmojis } from '@domternal/extension-emoji';
// Default: 218 popular emojiconst DefaultEmoji = Emoji.configure({ emojis });
// Extended: 516 emojiconst ExtendedEmoji = Emoji.configure({ emojis: allEmojis });You can also provide a fully custom dataset:
import type { EmojiItem } from '@domternal/extension-emoji';
const customEmojis: EmojiItem[] = [ { emoji: '\ud83d\ude80', name: 'rocket', shortcodes: ['rocket'], tags: ['launch', 'space'], group: 'Travel & Places', }, // ...];
const CustomEmoji = Emoji.configure({ emojis: customEmojis });EmojiItem interface
Section titled “EmojiItem interface”interface EmojiItem { emoji: string; // Native Unicode character (e.g., "\ud83d\ude04") name: string; // Unique name used as node attribute (e.g., "smile") shortcodes: string[]; // Shortcodes without colons (e.g., ["smile", "grinning"]) tags: string[]; // Search tags (e.g., ["happy", "face"]) group: string; // Category group (e.g., "Smileys & Emotion") fallbackImage?: string; // Optional fallback image URL for unsupported emoji}Emoticons
Section titled “Emoticons”When enableEmoticons: true, common text emoticons are converted to emoji on space:
| Emoticon | Emoji | Name |
|---|---|---|
:) :-) (: | \ud83d\ude42 | slightly smiling face |
:D :-D | \ud83d\ude03 | grinning face with big eyes |
;) ;-) | \ud83d\ude09 | winking face |
:( :-( | \ud83d\ude41 | slightly frowning face |
:'( | \ud83d\ude22 | crying face |
:P :-P :p | \ud83d\ude1b | face with tongue |
:O :-O :o | \ud83d\ude2e | face with open mouth |
:/ :-/ :\ | \ud83d\ude15 | confused face |
B) 8) | \ud83d\ude0e | smiling face with sunglasses |
:* :-* | \ud83d\ude17 | kissing face |
>:( >:-( | \ud83d\ude20 | angry face |
XD xD | \ud83d\ude02 | face with tears of joy |
:$ | \ud83d\ude33 | flushed face |
O:) | \ud83d\ude07 | smiling face with halo |
>:) | \ud83d\ude08 | smiling face with horns |
:| :-| | \ud83d\ude10 | neutral face |
<3 | \u2764\ufe0f | red heart |
</3 | \ud83d\udc94 | broken heart |
Emoticons must be followed by a space to trigger, and must be preceded by a space, the start of a line, or an atom node (an emoji, a mention, an inline image). They are not converted inside code blocks or inline code. The rule re-inserts the trailing space that triggered it, so a run of emoticons (xD xD, :) :)) converts one after another rather than every other one.
Plain text mode
Section titled “Plain text mode”const PlainEmoji = Emoji.configure({ plainText: true });When plainText: true, emoji are inserted as regular Unicode text characters instead of atom nodes. Shortcodes and emoticons still work, but the result is a text node (\ud83d\ude04) rather than a structured emoji node. This is useful when you need simple emoji input without node-level selection or custom rendering.
Attributes
Section titled “Attributes”| Attribute | Type | Default | Description |
|---|---|---|---|
name | string | null | null | The emoji name from the dataset (e.g., "grinning_face_with_smiling_eyes") |
The name attribute is stored as data-name in the HTML output. When parsing, both data-name and data-emoji-name (legacy format) are supported.
Commands
Section titled “Commands”insertEmoji
Section titled “insertEmoji”Insert an emoji by name at the current cursor position.
editor.commands.insertEmoji('grinning_face_with_smiling_eyes');editor.commands.insertEmoji('red_heart');editor.commands.insertEmoji('thumbs_up');
// With chainingeditor.chain().focus().insertEmoji('rocket').run();Returns false if:
- The emoji name is not found in the dataset
- The cursor is inside a code block
Inserting an emoji automatically tracks it in the frequency map (used by getFrequentlyUsed()).
suggestEmoji
Section titled “suggestEmoji”Programmatically open the suggestion dropdown by inserting the trigger character.
editor.commands.suggestEmoji();Returns false if the suggestion plugin is not configured (suggestion: null). When configured, inserts the trigger character (default :) at the cursor, which activates the suggestion plugin.
Keyboard shortcuts
Section titled “Keyboard shortcuts”Emoji does not register any keyboard shortcuts. The suggestion plugin intercepts these keys while active:
| Key | Action (when suggestion is open) |
|---|---|
ArrowDown | Select next item |
ArrowUp | Select previous item |
Enter | Insert selected emoji |
Escape | Close suggestion dropdown |
Input rules
Section titled “Input rules”Shortcodes
Section titled “Shortcodes”| Input | Result |
|---|---|
:smile: | \ud83d\ude04 (grinning face with smiling eyes) |
:heart: | \u2764\ufe0f (red heart) |
:thumbsup: | \ud83d\udc4d (thumbs up) |
The pattern :shortcode: matches any shortcode from the emoji dataset. Shortcodes are defined in each EmojiItem’s shortcodes array. The trailing : triggers the conversion.
Shortcode input rules are skipped inside code blocks and inline code marks.
Emoticons (when enabled)
Section titled “Emoticons (when enabled)”| Input | Result |
|---|---|
:) | \ud83d\ude42 (slightly smiling face) |
<3 | \u2764\ufe0f (red heart) |
:D | \ud83d\ude03 (grinning face with big eyes) |
Emoticons require enableEmoticons: true and are triggered by a trailing space. They must be preceded by a space, appear at the start of a line, or directly follow an atom node such as an emoji or mention.
Toolbar items
Section titled “Toolbar items”Main toolbar
Section titled “Main toolbar”Emoji registers a button in the toolbar with the name emoji in group insert at priority 50.
| Item | Command | Icon | Event |
|---|---|---|---|
| Insert Emoji | insertEmoji | smiley | insertEmoji |
Clicking the toolbar button emits an insertEmoji event. In a full setup, the Angular <domternal-emoji-picker> component (or a custom handler) listens for this event and opens a picker panel. The emitEvent property means the button does not execute the command directly.
Use the DomternalEmojiPicker class - it automatically listens for the insertEmoji event and renders a searchable picker panel:
import { Document, Paragraph, Text } from '@domternal/core';import { Emoji, emojis } from '@domternal/extension-emoji';import { DomternalEditor, DomternalEmojiPicker } from '@domternal/vanilla';import '@domternal/theme';
const pickerEl = document.createElement('div');document.body.appendChild(pickerEl);
const dm = new DomternalEditor(document.getElementById('editor')!, { extensions: [Document, Paragraph, Text, Emoji.configure({ emojis })],});
new DomternalEmojiPicker(pickerEl, { editor: dm.editor, emojis });Or handle the insertEmoji event yourself for a custom flow:
dm.editor.on('insertEmoji', ({ anchorElement }) => { dm.editor.commands.suggestEmoji(); // fall back to inline `:shortcode:` suggestion // or: showCustomEmojiPicker(anchorElement);});Use <domternal-emoji-picker> which automatically listens for the insertEmoji event:
<domternal-emoji-picker [editor]="editor()!" [emojis]="emojiData"/>Use <Domternal.EmojiPicker> which automatically listens for the insertEmoji event:
<Domternal.EmojiPicker emojis={emojis} />Use <Domternal.EmojiPicker> which automatically listens for the insertEmoji event:
<Domternal.EmojiPicker :emojis="emojis" />Without a toolbar, trigger the suggestion dropdown programmatically:
// Open the inline suggestion dropdowneditor.commands.suggestEmoji();
// Or insert an emoji directly by nameeditor.commands.insertEmoji('rocket');Suggestion plugin
Section titled “Suggestion plugin”The suggestion plugin provides inline autocomplete when the user types the trigger character (default :). It is a headless ProseMirror plugin with a pluggable renderer.
Configuration
Section titled “Configuration”import { Emoji, emojis, createEmojiSuggestionRenderer } from '@domternal/extension-emoji';
const EmojiWithSuggestion = Emoji.configure({ emojis, suggestion: { char: ':', // Trigger character (default) minQueryLength: 0, // Minimum chars before showing results (default) allowSpaces: false, // Allow spaces in query (default) render: createEmojiSuggestionRenderer(), // Vanilla DOM renderer items: ({ query }) => { // Optional custom filter // Return filtered/sorted EmojiItem[] }, },});SuggestionOptions
Section titled “SuggestionOptions”| Option | Type | Default | Description |
|---|---|---|---|
char | string | ':' | Trigger character that activates the suggestion |
minQueryLength | number | 0 | Minimum query length before showing suggestions |
allowSpaces | boolean | false | Allow spaces in the query string |
items | (props: { query: string }) => EmojiItem[] | undefined | Custom filter/sort function. Falls back to storage.searchEmoji() |
render | () => SuggestionRenderer | undefined | Factory that creates a renderer instance |
Query matching
Section titled “Query matching”The suggestion activates when:
- The trigger character is typed at the start of a line or after a space
- The query contains only alphanumeric characters, underscores, dashes, and plus signs
- The cursor is not inside a code block or inline code mark
The suggestion deactivates when:
- The user presses
Escape - The query no longer matches (e.g., cursor moves away)
- An emoji is inserted
Default renderer
Section titled “Default renderer”The built-in createEmojiSuggestionRenderer() renders a vanilla DOM dropdown:
- Shows up to 10 matching items
- Positioned below the cursor using Floating UI
- Fits the space below the caret: the available height is written to the
--dm-available-heightcustom property, and@domternal/themecaps the dropdown withmax-height: min(22rem, var(--dm-available-height, 100vh)). It flips above the caret only when less than 160px remains below (about five rows plus the dropdown chrome), so it does not jump over the text you just typed. Custom stylesheets that do not load@domternal/themeshould read the same property. NEW in v0.14.0. - Keyboard navigation:
ArrowUp/ArrowDownto select,Enterto insert - Mouse: click to insert, hover to highlight
- Appends inside
.dm-editorfor scroll-synchronized positioning - Shows “No emoji found” when no matches
Custom renderer
Section titled “Custom renderer”Implement the SuggestionRenderer interface for a custom UI:
import type { SuggestionRenderer, SuggestionProps } from '@domternal/extension-emoji';
function createCustomRenderer(): () => SuggestionRenderer { return () => { return { onStart(props: SuggestionProps) { // Create and show your dropdown UI // props.items - matching emoji // props.command(item) - call to insert an emoji // props.clientRect() - cursor position for positioning // props.element - ProseMirror editor DOM element }, onUpdate(props: SuggestionProps) { // Update your UI with new items/query }, onExit() { // Destroy your dropdown UI }, onKeyDown(event: KeyboardEvent): boolean { // Handle ArrowUp/Down/Enter - return true to prevent default return false; }, }; };}Storage
Section titled “Storage”The Emoji extension provides storage methods for searching and tracking emoji usage.
// Find an emoji by exact nameconst item = editor.storage.emoji.findEmoji('red_heart');// => { emoji: '\u2764\ufe0f', name: 'red_heart', shortcodes: ['heart', 'red_heart'], ... }
// Search emoji by name, shortcode, or tagconst results = editor.storage.emoji.searchEmoji('happy');// => [{ emoji: '\ud83d\ude00', name: 'grinning_face', ... }, ...]
// Get frequently used emoji names (sorted by usage count)const frequent = editor.storage.emoji.getFrequentlyUsed();// => ['red_heart', 'thumbs_up', 'smile', ...]
// Record a usage (done automatically by insertEmoji and input rules)editor.storage.emoji.addFrequentlyUsed('rocket');| Method | Returns | Description |
|---|---|---|
findEmoji(name) | EmojiItem | undefined | Find emoji by exact name |
searchEmoji(query) | EmojiItem[] | Search by name, shortcodes, or tags (case-insensitive) |
getFrequentlyUsed() | string[] | Emoji names sorted by usage count (most used first) |
addFrequentlyUsed(name) | void | Increment usage count for an emoji name |
Emoji picker component
Section titled “Emoji picker component”All four wrappers ship the same picker panel over the same markup and theme classes: <domternal-emoji-picker> in Angular, <Domternal.EmojiPicker> in React and Vue, and new DomternalEmojiPicker(el, { editor, emojis }) in Vanilla. Per-framework reference: Angular, React, Vue.
Setup (Angular)
Section titled “Setup (Angular)”import { DomternalEmojiPickerComponent } from '@domternal/angular';import { emojis } from '@domternal/extension-emoji';
@Component({ imports: [DomternalEmojiPickerComponent], template: ` <domternal-emoji-picker [editor]="editor()!" [emojis]="emojiData" /> `,})export class EditorComponent { emojiData = emojis;}| Prop | Type | Required | Description |
|---|---|---|---|
editor | Editor | Angular and Vanilla: yes | The Domternal editor instance. Optional in React and Vue, which fall back to the surrounding <Domternal> editor context |
emojis | EmojiPickerItem[] | Yes | Emoji dataset to display in the picker |
Features
Section titled “Features”- Search - real-time filtering by name, shortcode, and tags
- Category tabs - click to scroll to a category, auto-highlights on scroll
- Frequently used - shows up to 16 most-inserted emoji at the top
- Keyboard support -
Escapecloses the picker - Click outside - closes the picker
- Auto-positioning - uses Floating UI to position below the toolbar button
- Auto-focus - search input is focused when the picker opens
- Focus return - editor is re-focused when the picker closes
The picker listens for the insertEmoji event emitted by the toolbar button. It toggles open/closed on each click.
EmojiPickerItem interface
Section titled “EmojiPickerItem interface”interface EmojiPickerItem { emoji: string; // Native Unicode character name: string; // Unique name group: string; // Category for tab grouping}This is a subset of EmojiItem. You can pass the emojis or allEmojis arrays directly.
Styling
Section titled “Styling”Emoji styles are provided by @domternal/theme in _emoji-picker.scss:
Inline emoji node
Section titled “Inline emoji node”| Class | Description |
|---|---|
.emoji | Inline emoji span (cursor: default, no italic) |
.emoji.ProseMirror-selectednode | Selected state (accent-colored outline) |
Emoji picker
Section titled “Emoji picker”| Class | Description |
|---|---|
.dm-emoji-picker | Main picker panel (absolute, 20rem wide, max 22rem tall) |
.dm-emoji-picker-search | Search input container |
.dm-emoji-picker-tabs | Category tab bar |
.dm-emoji-picker-tab | Individual category tab (2rem square) |
.dm-emoji-picker-tab--active | Active tab state |
.dm-emoji-picker-grid | Scrollable emoji grid (8 columns) |
.dm-emoji-picker-category-label | Category heading in grid |
.dm-emoji-swatch | Individual emoji button (2rem square, hover: scale 1.15) |
.dm-emoji-picker-empty | ”No emoji found” message |
Suggestion dropdown
Section titled “Suggestion dropdown”| Class | Description |
|---|---|
.dm-emoji-suggestion | Suggestion dropdown container (absolute, 12-18rem wide) |
.dm-emoji-suggestion-item | Individual suggestion row |
.dm-emoji-suggestion-item--selected | Active/highlighted row |
.dm-emoji-suggestion-emoji | Emoji character in suggestion row |
.dm-emoji-suggestion-name | Emoji name text in suggestion row |
.dm-emoji-suggestion-empty | ”No emoji found” in suggestion |
All components support light and dark themes automatically via CSS custom properties (--dm-surface, --dm-border-color, --dm-text, --dm-hover, --dm-active, --dm-accent).
Exports
Section titled “Exports”import { Emoji, emojis, allEmojis, emoticons } from '@domternal/extension-emoji';import { createEmojiSuggestionRenderer } from '@domternal/extension-emoji';import { createSuggestionPlugin, emojiSuggestionPluginKey } from '@domternal/extension-emoji';import type { EmojiOptions, EmojiStorage, EmojiItem, SuggestionOptions, SuggestionProps, SuggestionRenderer,} from '@domternal/extension-emoji';| Export | Type | Description |
|---|---|---|
Emoji | Node extension | The emoji node extension |
emojis | EmojiItem[] | Default dataset (~218 popular emoji) |
allEmojis | EmojiItem[] | Extended dataset (~516 emoji) |
emoticons | Record<string, string> | Emoticon-to-name mapping (34 entries) |
createEmojiSuggestionRenderer | () => () => SuggestionRenderer | Factory for the vanilla DOM suggestion dropdown |
createSuggestionPlugin | (options) => Plugin | Low-level ProseMirror suggestion plugin factory |
emojiSuggestionPluginKey | PluginKey | Plugin key for accessing suggestion state |
EmojiOptions | TypeScript type | Options for Emoji.configure() |
EmojiStorage | TypeScript type | Storage methods interface |
EmojiItem | TypeScript type | Emoji data item interface |
SuggestionOptions | TypeScript type | Suggestion plugin options |
SuggestionProps | TypeScript type | Props passed to suggestion renderer callbacks |
SuggestionRenderer | TypeScript type | Renderer interface for custom suggestion UI |
JSON representation
Section titled “JSON representation”An emoji node:
{ "type": "emoji", "attrs": { "name": "grinning_face_with_smiling_eyes" }}A paragraph with text and emoji:
{ "type": "paragraph", "content": [ { "type": "text", "text": "Hello " }, { "type": "emoji", "attrs": { "name": "waving_hand" } }, { "type": "text", "text": " welcome!" } ]}A document with multiple emoji:
{ "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Great job " }, { "type": "emoji", "attrs": { "name": "thumbs_up" } }, { "type": "emoji", "attrs": { "name": "party_popper" } } ] } ]}Source
Section titled “Source”@domternal/extension-emoji - Emoji.ts
See also
Section titled “See also”- Mention -
@mention suggestions with custom data source - Typography - smart-quote and dash auto-replacement rules