Mentions (@)
Mentions enable quick inline user suggestions when typing @. As users type, a debounced autocomplete dropdown appears at the cursor location. Selecting a user inserts a non-editable chip element complete with an accessible hover user card.
Mentions are disabled by default. Enable them with mentions: true or with an object whose enabled value is true. The trigger is always @ and is not configurable.
Key Highlights
Fixed @ Trigger
Typing @ opens an autocomplete dropdown at the current cursor position.
Debounced Async Fetching
Automatically debounces network requests (200ms default) and injects AbortSignal to cancel stale requests when users type rapidly.
Session Caching
Identical search queries within the active editor session are cached locally to minimize backend server load.
Keyboard Navigation
Use Up / Down arrows to navigate suggestions, Enter or Tab to insert, and Esc to dismiss.
Static Mentions
To use a fixed list of users, pass an array of MentionItem objects in init.mentions.items:
<script setup lang="ts">
import { shallowRef } from 'vue';
import {
Editor,
type EditorInit,
type MentionItem,
} from '@erag/text-editor-vue';
const content = shallowRef('');
const mentionItems: MentionItem[] = [
{
id: 1,
label: 'Damon Cross',
description: 'Senior Backend Developer',
avatar: 'https://i.pravatar.cc/96?img=12',
value: 'damon@example.com',
},
{
id: 2,
label: 'Daniel Long',
description: 'Operations Manager',
avatar: 'https://i.pravatar.cc/96?img=11',
value: 'daniel@example.com',
},
{
id: 5,
label: 'Amit Gupta',
description: 'Senior Software Developer',
avatar: 'https://i.pravatar.cc/96?img=14',
value: 'amit@example.com',
},
];
const editorConfig: EditorInit = {
mentions: {
enabled: true,
minimumCharacters: 0,
debounce: 200,
limit: 8,
items: mentionItems,
},
};
</script>
<template>
<Editor v-model="content" :init="editorConfig" />
</template>Static mentions search case-insensitively across label, description, and value. Matches starting with the search query appear first.
Async Mentions (Fetch API)
For dynamic user searches from a database or API endpoint, pass an asynchronous function to items:
<script setup lang="ts">
import { shallowRef } from 'vue';
import {
Editor,
type EditorInit,
type MentionItem,
} from '@erag/text-editor-vue';
const content = shallowRef('');
// Async fetcher function
async function fetchUserMentions(
query: string,
signal: AbortSignal,
): Promise<MentionItem[]> {
const response = await fetch(
`/api/users/search?q=${encodeURIComponent(query)}`,
{
signal,
headers: { Accept: 'application/json' },
},
);
if (!response.ok) {
throw new Error('Mention search failed');
}
interface UserResponse {
id: number;
name: string;
designation?: string;
avatar?: string;
email?: string;
}
const users = (await response.json()) as UserResponse[];
return users.map((user) => ({
id: user.id,
label: user.name,
description: user.designation,
avatar: user.avatar,
value: user.email,
}));
}
// Editor configuration
const editorConfig: EditorInit = {
mentions: {
enabled: true,
debounce: 200,
items: fetchUserMentions,
},
};
</script>
<template>
<Editor v-model="content" :init="editorConfig" />
</template>Key Features
- Debounce Delay (
debounce: 200): Prevents spamming your backend by waiting 200ms after keypress. - Auto Cancellation (
AbortSignal): Automatically cancels outdated HTTP requests when the user continues typing. - Session Cache: Caches duplicate queries locally so repeated searches hit memory instead of the network.
Mention Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enables mention autocomplete when typing @. |
minimumCharacters | number | 0 | Characters required after @ before searching starts. |
debounce | number | 200 | Search debounce delay in milliseconds. |
limit | number | 8 | Maximum visible options in dropdown list. |
items | MentionItem[] | Function | [] | Static item array or async (query, signal) => Promise<MentionItem[]> function. |
The detector only opens for a standalone mention query such as @, @da, or Hello @damon. It does not activate inside links, inline code, <pre> blocks, existing mentions, non-editable elements, email addresses, URL paths, disabled editors, or readonly editors. Whitespace closes the query.
Dropdown states and controls
The caret-positioned dropdown supports loading, empty, error, and results states. Failed async requests show a Retry action. The first result is active automatically, and the dropdown flips above the caret when there is not enough viewport space below it.
| Key | Behavior |
|---|---|
ArrowDown | Select the next result, wrapping at the end. |
ArrowUp | Select the previous result, wrapping at start. |
Home | Select the first result. |
End | Select the last result. |
Enter | Insert the active result. |
Tab | Insert the active result without moving focus. |
Escape | Close without inserting. |
Hovering a result updates the active option. Clicking uses pointer handling that preserves the editor selection, so the mention is inserted once at the original query range.
Hover card
Hover an inserted mention to display its known avatar, label, description, and value. If the avatar is missing or cannot load, the card shows initials. The card remains inside the viewport and uses the item data cached during the current editor session.
Mention Lifecycle Events
Track mention search, insertion, and deletion events in your application:
<script setup lang="ts">
import { shallowRef } from 'vue';
import {
Editor,
type MentionSelectEvent,
type MentionRemoveEvent,
} from '@erag/text-editor-vue';
const content = shallowRef('');
function handleMentionSelect(event: MentionSelectEvent) {
console.log(`Selected ${event.item.label} using query "@${event.query}".`);
}
function handleMentionRemove(event: MentionRemoveEvent) {
console.log(`Removed mention chip for ${event.item.label}.`);
}
</script>
<template>
<Editor
v-model="content"
:init="editorConfig"
@mention-select="handleMentionSelect"
@mention-remove="handleMentionRemove"
/>
</template>mention-search: Fired with{ query }when debounced search starts.mention-select: Fired with{ item, query }when a user selects a mention option.mention-remove: Fired with{ item }whenBackspaceorDeleteremoves an inserted mention chip.
Mention slots
The editor supplies a complete default UI, but these slots can customize dropdown content without replacing its keyboard handling:
<Editor v-model="content" :init="editorConfig">
<template #mention-item="{ item, active }">
<span>{{ active ? '→' : '' }} {{ item.label }}</span>
</template>
<template #mention-loading="{ query }">Loading {{ query }}…</template>
<template #mention-empty="{ query }">No result for {{ query }}</template>
<template #mention-error="{ query, retry }">
<button type="button" @click="retry">Retry {{ query }}</button>
</template>
</Editor>HTML Output Format
Inserted mentions render as non-editable inline chips:
<span
class="erag-mention"
data-erag-mention="true"
data-erag-mention-id="1"
data-erag-mention-label="Damon Cross"
data-erag-mention-value="damon@example.com"
contenteditable="false"
>@Damon Cross</span
>When value is absent, data-erag-mention-value is omitted. Pressing Backspace immediately after a mention or Delete immediately before it removes the complete chip and keeps the removal undoable.