Skip to content

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:

vue
<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:

vue
<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

  1. Debounce Delay (debounce: 200): Prevents spamming your backend by waiting 200ms after keypress.
  2. Auto Cancellation (AbortSignal): Automatically cancels outdated HTTP requests when the user continues typing.
  3. Session Cache: Caches duplicate queries locally so repeated searches hit memory instead of the network.

Mention Configuration Options

OptionTypeDefaultDescription
enabledbooleantrueEnables mention autocomplete when typing @.
minimumCharactersnumber0Characters required after @ before searching starts.
debouncenumber200Search debounce delay in milliseconds.
limitnumber8Maximum visible options in dropdown list.
itemsMentionItem[] | 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.

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.

KeyBehavior
ArrowDownSelect the next result, wrapping at the end.
ArrowUpSelect the previous result, wrapping at start.
HomeSelect the first result.
EndSelect the last result.
EnterInsert the active result.
TabInsert the active result without moving focus.
EscapeClose 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:

vue
<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 } when Backspace or Delete removes 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:

vue
<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:

html
<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.

Released under the MIT License. Copyright © Er Amit Gupta