Skip to content

Mention

The Mention extension adds inline @mention nodes with multi-trigger support, a headless suggestion plugin for autocomplete dropdowns, async item fetching with debounce, and configurable rendering. The extension ships as a separate package (@domternal/extension-mention).

Terminal window
pnpm add @domternal/extension-mention

Type @ followed by a name to open the suggestion dropdown. Use arrow keys to navigate and Enter to insert.

With the default theme enabled. Type @ followed by a name to trigger the suggestion dropdown.

Click to try it out
import { Editor, Document, Paragraph, Text } from '@domternal/core';
import { Mention, createMentionSuggestionRenderer } from '@domternal/extension-mention';
import '@domternal/theme';
const users = [
{ id: '1', label: 'Alice Johnson' },
{ id: '2', label: 'Bob Smith' },
{ id: '3', label: 'Charlie Brown' },
];
const editor = new Editor({
element: document.getElementById('editor')!,
extensions: [
Document, Paragraph, Text,
Mention.configure({
suggestion: {
char: '@',
name: 'user',
items: ({ query }) => users.filter(u =>
u.label.toLowerCase().includes(query.toLowerCase()),
),
render: createMentionSuggestionRenderer(),
},
}),
],
content: '<p>Type @ to mention someone!</p>',
});

Type @ followed by a name to see the suggestion dropdown. The built-in renderer (createMentionSuggestionRenderer) provides a styled dropdown with keyboard navigation.

PropertyValue
ProseMirror namemention
TypeNode
Groupinline
ContentNone (atom node)
AtomYes
InlineYes
SelectableNo
DraggableNo
HTML tag<span> (parses span[data-type="mention"] and span[data-mention])

Mention is an inline atom node. It cannot contain text or other nodes. Unlike emoji, mention nodes are not selectable - pressing Backspace with the cursor after a mention deletes it.

OptionTypeDefaultDescription
suggestionMentionTrigger | nullnullSingle trigger config (shorthand). Ignored if triggers is non-empty
triggersMentionTrigger[][]Multiple trigger configs. Takes precedence over suggestion
deleteTriggerWithBackspacebooleanfalseDelete trigger char alongside mention on Backspace
renderHTML(props) => DOMOutputSpec | nullnullCustom HTML render function (e.g., render as <a>)
renderText(props) => string | nullnullCustom plain text render function
HTMLAttributesRecord<string, unknown>{}HTML attributes added to the mention <span> element

Use suggestion for a single trigger (most common). Use triggers for multiple trigger characters:

// Single trigger (shorthand)
Mention.configure({
suggestion: {
char: '@',
name: 'user',
items: ({ query }) => fetchUsers(query),
render: createMentionSuggestionRenderer(),
},
});
// Multiple triggers
Mention.configure({
triggers: [
{
char: '@',
name: 'user',
items: ({ query }) => fetchUsers(query),
render: createMentionSuggestionRenderer(),
},
{
char: '#',
name: 'tag',
items: ({ query }) => fetchTags(query),
render: createMentionSuggestionRenderer(),
},
],
});

Each trigger creates its own ProseMirror plugin instance with a unique PluginKey.

Override how mentions render in HTML or plain text:

Mention.configure({
renderHTML: ({ node, options, HTMLAttributes }) => {
return ['a', {
...HTMLAttributes,
href: `/users/${node.attrs.id}`,
class: 'mention-link',
}, `@${node.attrs.label}`];
},
renderText: ({ node, options }) => `@${node.attrs.label}`,
});
AttributeTypeDefaultHTML attributeDescription
idstring | nullnulldata-idUnique identifier (e.g., user ID)
labelstring | nullnulldata-labelDisplay text (e.g., “Alice Johnson”)
typestring'mention'data-mention-typeTrigger type name (e.g., “user”, “tag”)

The type attribute is set automatically from the trigger’s name property when inserting via the suggestion plugin.

Insert a mention node at the current cursor position.

editor.commands.insertMention({ id: '1', label: 'Alice Johnson' });
editor.commands.insertMention({ id: '1', label: 'Alice Johnson', type: 'user' });
// With chaining
editor.chain().focus().insertMention({ id: '1', label: 'Alice Johnson' }).run();

Returns false if id or label is empty.

Delete a mention node by ID or at the cursor position.

// Delete mention at cursor (must be right after the mention)
editor.commands.deleteMention();
// Delete first mention with a specific ID
editor.commands.deleteMention('user-123');

Returns false if:

  • No ID is provided and the selection is not empty or the node before the cursor is not a mention
  • The specified ID is not found in the document
KeyAction
BackspaceDelete mention node before cursor. If deleteTriggerWithBackspace is enabled, also removes the trigger character

The suggestion plugin intercepts these keys while the dropdown is open:

KeyAction (when suggestion is open)
ArrowDownSelect next item
ArrowUpSelect previous item
EnterInsert selected mention
EscapeClose suggestion dropdown

Mention does not have input rules. The suggestion plugin handles trigger character detection.

Mention does not register any toolbar items. Mentions are typically triggered by typing the trigger character, not through toolbar buttons.

The suggestion plugin provides inline autocomplete when the user types a trigger character (e.g., @). It is a headless ProseMirror plugin with a pluggable renderer.

OptionTypeDefaultDescription
charstring(required)Trigger character (e.g., '@', '#')
namestring(required)Unique trigger name, stored as data-mention-type
items(props: { query, trigger }) => MentionItem[] | Promise<MentionItem[]>(required)Fetch items matching a query. Supports sync and async
render() => MentionSuggestionRendererundefinedFactory that creates a renderer instance
minQueryLengthnumber0Minimum query length before showing suggestions
allowSpacesbooleanfalseAllow spaces in the query string
appendTextstring' ' (space)Text appended after inserting a mention
invalidNodesstring[][]Node types where suggestion should not activate
debouncenumber0Debounce delay (ms) for items() calls. Use >0 for API-backed items
decorationClassstring'mention-suggestion'CSS class for the inline decoration on active trigger+query
decorationTagstring'span'HTML tag for the inline decoration element
shouldShow(props) => booleanundefinedCustom visibility control. Return false to suppress
interface MentionItem {
id: string; // Unique identifier
label: string; // Display text
[key: string]: unknown; // Extra data (avatar, role, email, etc.)
}

For API-backed suggestions, return a Promise from items() and set debounce to avoid hammering the server:

Mention.configure({
suggestion: {
char: '@',
name: 'user',
debounce: 200,
items: async ({ query }) => {
const res = await fetch(`/api/users?q=${encodeURIComponent(query)}`);
return res.json();
},
render: createMentionSuggestionRenderer(),
},
});

Errors from async items() are swallowed - the suggestion stays active with no items.

The suggestion activates when:

  1. The trigger character is typed at the start of a line or after a space
  2. The query contains only alphanumeric characters, underscores, dashes, dots, and plus signs
  3. The cursor is not inside a code block (spec.code), inline code mark, or a node listed in invalidNodes

The suggestion deactivates when:

  • The user presses Escape
  • The query no longer matches (e.g., cursor moves away, special characters typed)
  • A mention is inserted

Use shouldShow to suppress suggestions in specific contexts:

Mention.configure({
suggestion: {
// ... other options
shouldShow: ({ state, view }) => {
// Suppress suggestions in collaboration when triggered by remote cursor
return state.selection.empty && view.hasFocus();
},
},
});

The built-in createMentionSuggestionRenderer() renders a vanilla DOM dropdown:

  • Shows up to 8 matching items
  • Positioned below the cursor using Floating UI
  • Keyboard navigation: ArrowUp/ArrowDown to select, Enter to insert
  • Mouse: click to insert, hover to highlight
  • Appends inside .dm-editor for scroll-synchronized positioning
  • Shows “No results” when no matches
  • ARIA: role="listbox" on container, role="option" on items

Implement the MentionSuggestionRenderer interface for a custom UI:

import type { MentionSuggestionRenderer, MentionSuggestionProps } from '@domternal/extension-mention';
function createCustomRenderer(): () => MentionSuggestionRenderer {
return () => {
return {
onStart(props: MentionSuggestionProps) {
// Create and show your dropdown UI
// props.items - matching mention items
// props.command(item) - call to insert a mention
// props.clientRect() - cursor position for positioning
// props.element - ProseMirror editor DOM element
},
onUpdate(props: MentionSuggestionProps) {
// 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;
},
};
};
}

Dismiss the suggestion dropdown programmatically:

import { dismissMentionSuggestion } from '@domternal/extension-mention';
dismissMentionSuggestion(editor.view, 'user');

While the suggestion is active, an inline decoration is applied to the trigger+query range. This lets you style the in-progress mention text (e.g., underline it):

.mention-suggestion {
text-decoration: underline;
text-decoration-color: var(--dm-accent);
}

Customize the decoration class and tag with decorationClass and decorationTag options.

The Mention extension provides a findMentions() method to scan the document for all mention nodes.

const mentions = editor.storage.mention.findMentions();
// => [
// { id: '1', label: 'Alice Johnson', type: 'user', pos: 7 },
// { id: '7', label: 'Grace Hopper', type: 'user', pos: 35 },
// ]
MethodReturnsDescription
findMentions()Array<{ id, label, type, pos }>All mention nodes with document positions

Use this to extract mentioned users for notifications, permissions, or other backend processing.

Mention styles are provided by @domternal/theme in _mention.scss:

ClassDescription
.mentionMention span (accent background, rounded, 500 weight)
.mention-suggestionIn-progress decoration (underline with accent color)
ClassDescription
.dm-mention-suggestionDropdown container (absolute, 12-20rem wide)
.dm-mention-suggestion-itemIndividual suggestion row
.dm-mention-suggestion-item--selectedActive/highlighted row
.dm-mention-suggestion-labelLabel text in suggestion row
.dm-mention-suggestion-empty”No results” message
VariableDefaultDescription
--dm-mention-bgvar(--dm-accent-surface)Mention background color
--dm-mention-colorvar(--dm-accent)Mention text color
--dm-mention-border-radius0.25remMention border radius

All components support light and dark themes automatically via CSS custom properties.

import { Mention, createMentionSuggestionRenderer } from '@domternal/extension-mention';
import { createMentionSuggestionPlugin, dismissMentionSuggestion } from '@domternal/extension-mention';
import type {
MentionOptions, MentionStorage, MentionItem,
MentionTrigger, MentionSuggestionProps, MentionSuggestionRenderer,
} from '@domternal/extension-mention';
ExportTypeDescription
MentionNode extensionThe mention node extension (also default export)
createMentionSuggestionRenderer() => () => MentionSuggestionRendererFactory for the vanilla DOM suggestion dropdown
createMentionSuggestionPlugin(options) => PluginLow-level ProseMirror suggestion plugin factory
dismissMentionSuggestion(view, triggerName) => voidProgrammatically dismiss the suggestion
MentionOptionsTypeScript typeOptions for Mention.configure()
MentionStorageTypeScript typeStorage methods interface
MentionItemTypeScript typeMention data item interface
MentionTriggerTypeScript typeTrigger configuration interface
MentionSuggestionPropsTypeScript typeProps passed to suggestion renderer callbacks
MentionSuggestionRendererTypeScript typeRenderer interface for custom suggestion UI

A mention node:

{
"type": "mention",
"attrs": {
"id": "1",
"label": "Alice Johnson",
"type": "user"
}
}

A paragraph with text and a mention:

{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Talk to " },
{
"type": "mention",
"attrs": { "id": "1", "label": "Alice Johnson", "type": "user" }
},
{ "type": "text", "text": " about this." }
]
}

A document with multiple mentions:

{
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "CC " },
{ "type": "mention", "attrs": { "id": "1", "label": "Alice Johnson", "type": "user" } },
{ "type": "text", "text": " and " },
{ "type": "mention", "attrs": { "id": "7", "label": "Grace Hopper", "type": "user" } }
]
}
]
}

@domternal/extension-mention - Mention.ts