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).
Installation
Section titled “Installation”pnpm add @domternal/extension-mentionLive Playground
Section titled “Live Playground”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.
Vanilla JS preview - Angular components produce the same output
Vanilla JS preview - React components produce the same output
Plain mention without the theme. The buttons above the editor call commands like insertMention() and findMentions() directly. Type @ to see inline suggestions.
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.
import { Component, signal } from '@angular/core';import { DomternalEditorComponent, DomternalToolbarComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text } from '@domternal/core';import { Mention, createMentionSuggestionRenderer } from '@domternal/extension-mention';import type { MentionItem } from '@domternal/extension-mention';
const users: MentionItem[] = [ { id: '1', label: 'Alice Johnson' }, { id: '2', label: 'Bob Smith' }, { id: '3', label: 'Charlie Brown' },];
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent, DomternalToolbarComponent], templateUrl: './editor.html',})export class EditorComponent { editor = signal<Editor | null>(null); 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>';}@if (editor(); as ed) { <domternal-toolbar [editor]="ed" />}<domternal-editor [extensions]="extensions" [content]="content" (editorCreated)="editor.set($event)"/>import { Domternal } from '@domternal/react';import { Document, Paragraph, Text } from '@domternal/core';import { Mention, createMentionSuggestionRenderer } from '@domternal/extension-mention';
const users = [ { id: '1', label: 'Alice Johnson' }, { id: '2', label: 'Bob Smith' }, { id: '3', label: 'Charlie Brown' },];
export default function Editor() { return ( <Domternal 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>" > <Domternal.Toolbar /> <Domternal.Content /> </Domternal> );}Type @ followed by a name to see the suggestion dropdown. The built-in renderer provides a styled dropdown with keyboard navigation.
import { Editor, Document, Paragraph, Text } from '@domternal/core';import { Mention, createMentionSuggestionRenderer } from '@domternal/extension-mention';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [ Document, Paragraph, Text, Mention.configure({ suggestion: { char: '@', name: 'user', items: ({ query }) => [ { id: '1', label: 'Alice Johnson' }, { id: '2', label: 'Bob Smith' }, ].filter(u => u.label.toLowerCase().includes(query.toLowerCase())), render: createMentionSuggestionRenderer(), }, }), ],});
// Insert a mention programmaticallyeditor.commands.insertMention({ id: '1', label: 'Alice Johnson', type: 'user' });Schema
Section titled “Schema”| Property | Value |
|---|---|
| ProseMirror name | mention |
| Type | Node |
| Group | inline |
| Content | None (atom node) |
| Atom | Yes |
| Inline | Yes |
| Selectable | No |
| Draggable | No |
| 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.
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
suggestion | MentionTrigger | null | null | Single trigger config (shorthand). Ignored if triggers is non-empty |
triggers | MentionTrigger[] | [] | Multiple trigger configs. Takes precedence over suggestion |
deleteTriggerWithBackspace | boolean | false | Delete trigger char alongside mention on Backspace |
renderHTML | (props) => DOMOutputSpec | null | null | Custom HTML render function (e.g., render as <a>) |
renderText | (props) => string | null | null | Custom plain text render function |
HTMLAttributes | Record<string, unknown> | {} | HTML attributes added to the mention <span> element |
Single vs. multiple triggers
Section titled “Single vs. multiple triggers”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 triggersMention.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.
Custom rendering
Section titled “Custom rendering”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}`,});Attributes
Section titled “Attributes”| Attribute | Type | Default | HTML attribute | Description |
|---|---|---|---|---|
id | string | null | null | data-id | Unique identifier (e.g., user ID) |
label | string | null | null | data-label | Display text (e.g., “Alice Johnson”) |
type | string | 'mention' | data-mention-type | Trigger type name (e.g., “user”, “tag”) |
The type attribute is set automatically from the trigger’s name property when inserting via the suggestion plugin.
Commands
Section titled “Commands”insertMention
Section titled “insertMention”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 chainingeditor.chain().focus().insertMention({ id: '1', label: 'Alice Johnson' }).run();Returns false if id or label is empty.
deleteMention
Section titled “deleteMention”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 IDeditor.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
Keyboard shortcuts
Section titled “Keyboard shortcuts”| Key | Action |
|---|---|
Backspace | Delete mention node before cursor. If deleteTriggerWithBackspace is enabled, also removes the trigger character |
The suggestion plugin intercepts these keys while the dropdown is open:
| Key | Action (when suggestion is open) |
|---|---|
ArrowDown | Select next item |
ArrowUp | Select previous item |
Enter | Insert selected mention |
Escape | Close suggestion dropdown |
Input rules
Section titled “Input rules”Mention does not have input rules. The suggestion plugin handles trigger character detection.
Toolbar items
Section titled “Toolbar items”Mention does not register any toolbar items. Mentions are typically triggered by typing the trigger character, not through toolbar buttons.
Suggestion plugin
Section titled “Suggestion plugin”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.
MentionTrigger options
Section titled “MentionTrigger options”| Option | Type | Default | Description |
|---|---|---|---|
char | string | (required) | Trigger character (e.g., '@', '#') |
name | string | (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 | () => MentionSuggestionRenderer | undefined | Factory that creates a renderer instance |
minQueryLength | number | 0 | Minimum query length before showing suggestions |
allowSpaces | boolean | false | Allow spaces in the query string |
appendText | string | ' ' (space) | Text appended after inserting a mention |
invalidNodes | string[] | [] | Node types where suggestion should not activate |
debounce | number | 0 | Debounce delay (ms) for items() calls. Use >0 for API-backed items |
decorationClass | string | 'mention-suggestion' | CSS class for the inline decoration on active trigger+query |
decorationTag | string | 'span' | HTML tag for the inline decoration element |
shouldShow | (props) => boolean | undefined | Custom visibility control. Return false to suppress |
MentionItem interface
Section titled “MentionItem interface”interface MentionItem { id: string; // Unique identifier label: string; // Display text [key: string]: unknown; // Extra data (avatar, role, email, etc.)}Async items with debounce
Section titled “Async items with debounce”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.
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, dots, and plus signs
- The cursor is not inside a code block (
spec.code), inline code mark, or a node listed ininvalidNodes
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
shouldShow
Section titled “shouldShow”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(); }, },});Default renderer
Section titled “Default renderer”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/ArrowDownto select,Enterto insert - Mouse: click to insert, hover to highlight
- Appends inside
.dm-editorfor scroll-synchronized positioning - Shows “No results” when no matches
- ARIA:
role="listbox"on container,role="option"on items
Custom renderer
Section titled “Custom renderer”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; }, }; };}Programmatic dismiss
Section titled “Programmatic dismiss”Dismiss the suggestion dropdown programmatically:
import { dismissMentionSuggestion } from '@domternal/extension-mention';
dismissMentionSuggestion(editor.view, 'user');Inline decoration
Section titled “Inline decoration”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.
Storage
Section titled “Storage”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 },// ]| Method | Returns | Description |
|---|---|---|
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.
Styling
Section titled “Styling”Mention styles are provided by @domternal/theme in _mention.scss:
Inline mention node
Section titled “Inline mention node”| Class | Description |
|---|---|
.mention | Mention span (accent background, rounded, 500 weight) |
.mention-suggestion | In-progress decoration (underline with accent color) |
Suggestion dropdown
Section titled “Suggestion dropdown”| Class | Description |
|---|---|
.dm-mention-suggestion | Dropdown container (absolute, 12-20rem wide) |
.dm-mention-suggestion-item | Individual suggestion row |
.dm-mention-suggestion-item--selected | Active/highlighted row |
.dm-mention-suggestion-label | Label text in suggestion row |
.dm-mention-suggestion-empty | ”No results” message |
CSS custom properties
Section titled “CSS custom properties”| Variable | Default | Description |
|---|---|---|
--dm-mention-bg | var(--dm-accent-surface) | Mention background color |
--dm-mention-color | var(--dm-accent) | Mention text color |
--dm-mention-border-radius | 0.25rem | Mention border radius |
All components support light and dark themes automatically via CSS custom properties.
Exports
Section titled “Exports”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';| Export | Type | Description |
|---|---|---|
Mention | Node extension | The mention node extension (also default export) |
createMentionSuggestionRenderer | () => () => MentionSuggestionRenderer | Factory for the vanilla DOM suggestion dropdown |
createMentionSuggestionPlugin | (options) => Plugin | Low-level ProseMirror suggestion plugin factory |
dismissMentionSuggestion | (view, triggerName) => void | Programmatically dismiss the suggestion |
MentionOptions | TypeScript type | Options for Mention.configure() |
MentionStorage | TypeScript type | Storage methods interface |
MentionItem | TypeScript type | Mention data item interface |
MentionTrigger | TypeScript type | Trigger configuration interface |
MentionSuggestionProps | TypeScript type | Props passed to suggestion renderer callbacks |
MentionSuggestionRenderer | TypeScript type | Renderer interface for custom suggestion UI |
JSON representation
Section titled “JSON representation”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" } } ] } ]}Source
Section titled “Source”@domternal/extension-mention - Mention.ts