Skip to main content

Overview

The Mention plugin provides inline mentions for users, channels, pages, or custom entities. Features autocomplete dropdown, multiple trigger characters, and flexible search integration.

Installation

npm install @yoopta/mention

Basic Usage

import { useMemo } from 'react';
import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import Mention from '@yoopta/mention';
import Paragraph from '@yoopta/paragraph';

const mentionPlugin = Mention.extend({
  options: {
    onSearch: async (query, trigger) => {
      // Fetch matching users/entities
      const response = await fetch(`/api/search?q=${query}&type=${trigger.type}`);
      return response.json();
    },
  },
});

const plugins = [Paragraph, mentionPlugin];

export default function Editor() {
  const editor = useMemo(() => createYooptaEditor({ plugins, marks: [] }), []);
  return <YooptaEditor editor={editor} onChange={() => {}} />;
}

Features

  • Multiple Triggers: Support @, #, [[, or custom trigger characters
  • Autocomplete: Dropdown with search results
  • Type Support: Users, channels, pages, custom types
  • Keyboard Navigation: Arrow keys, Enter, Escape
  • Debounced Search: Configurable search debouncing
  • Custom Rendering: Full control over mention and dropdown UI

Configuration

Single Trigger

import Mention from '@yoopta/mention';

const mentionPlugin = Mention.extend({
  options: {
    char: '@',
    onSearch: async (query) => {
      return [
        { id: '1', name: 'John Doe', avatar: '/avatars/john.jpg' },
        { id: '2', name: 'Jane Smith', avatar: '/avatars/jane.jpg' },
      ];
    },
  },
});

Multiple Triggers

const mentionPlugin = Mention.extend({
  options: {
    triggers: [
      { char: '@', type: 'user', allowSpaces: false },
      { char: '#', type: 'channel', allowSpaces: false },
      { char: '[[', type: 'page', allowSpaces: true },
    ],
    onSearch: async (query, trigger) => {
      // Different search based on trigger type
      if (trigger.type === 'user') {
        return fetchUsers(query);
      } else if (trigger.type === 'channel') {
        return fetchChannels(query);
      } else if (trigger.type === 'page') {
        return fetchPages(query);
      }
    },
  },
});

Options

triggers
MentionTrigger[]
Multiple trigger configurations
char
string
default:"@"
Single trigger character (shorthand for triggers: [{ char }])
Search function called when user types after triggerReturns: Array of mention items with id, name, optional avatar, type, and meta
debounceMs
number
default:"300"
Debounce delay for search in milliseconds
minQueryLength
number
default:"0"
Minimum query length before triggering search
onSelect
(item: MentionItem, trigger: MentionTrigger) => void
Called when a mention is selected
onOpen
(trigger: MentionTrigger) => void
Called when dropdown opens
onClose
() => void
Called when dropdown closes
closeOnSelect
boolean
default:"true"
Close dropdown when item is selected
closeOnClickOutside
boolean
default:"true"
Close dropdown on click outside
closeOnEscape
boolean
default:"true"
Close dropdown on Escape key

Element Props

mention.props
object

Commands

Mention commands are available via MentionCommands.

insertMention

Insert a mention at the current selection.
import Mention, { MentionCommands } from '@yoopta/mention';

MentionCommands.insertMention(editor, {
  slate: slateEditor,
  item: {
    id: '123',
    name: 'John Doe',
    type: 'user',
    avatar: '/avatars/john.jpg',
  },
});

deleteMention

Remove a mention.
import { MentionCommands } from '@yoopta/mention';

MentionCommands.deleteMention(editor, {
  slate: slateEditor,
});

buildMentionElements

Build mention element structure (used internally).
import { MentionCommands } from '@yoopta/mention';

const mentionElement = MentionCommands.buildMentionElements(editor, {
  item: { id: '123', name: 'John Doe' },
});

Hooks

useMentionDropdown

Hook for building custom mention dropdown UI.
import { useMentionDropdown } from '@yoopta/mention';

function MentionDropdown() {
  const {
    isOpen,
    query,
    trigger,
    items,
    loading,
    error,
    selectedIndex,
    setSelectedIndex,
    selectItem,
    close,
    refs,
    floatingStyles,
  } = useMentionDropdown(editor);

  if (!isOpen) return null;

  return (
    <div ref={refs.setFloating} style={floatingStyles}>
      {loading && <div>Loading...</div>}
      {items.map((item, index) => (
        <div
          key={item.id}
          onClick={() => selectItem(item)}
          className={selectedIndex === index ? 'selected' : ''}
        >
          {item.avatar && <img src={item.avatar} />}
          {item.name}
        </div>
      ))}
    </div>
  );
}

useMentionState

Access mention state.
import { useMentionState } from '@yoopta/mention';

const { isOpen, query, trigger, targetRect, triggerRange } = useMentionState(editor);

useMentionTriggerActive

Check if a specific trigger is active.
import { useMentionTriggerActive } from '@yoopta/mention';

const isUserMentionActive = useMentionTriggerActive(editor, '@');

Editor Extension

The mention plugin extends the editor with mention-specific methods:
import { MentionYooEditor } from '@yoopta/mention';

const editor = createYooptaEditor({ plugins }) as MentionYooEditor;

// Access mention state
editor.mentions.state.isOpen;
editor.mentions.state.query;

// Control dropdown
editor.mentions.open({ trigger, targetRect, triggerRange });
editor.mentions.close('escape');
editor.mentions.setQuery('john');

// Select current item (set by useMentionDropdown)
editor.mentions.selectCurrentItem?.();

Types

MentionItem

type MentionItem<TMeta = Record<string, unknown>> = {
  id: string;
  type?: MentionType;
  name: string;
  avatar?: string;
  meta?: TMeta;
};

MentionType

type MentionType = 'user' | 'channel' | 'page' | 'custom' | string;

Event Types

The plugin emits the following events:
  • mention:open - Dropdown opened
  • mention:close - Dropdown closed
  • mention:query-change - Search query changed
  • mention:select - Item selected

Examples

User Mentions with API

const mentionPlugin = Mention.extend({
  options: {
    char: '@',
    onSearch: async (query) => {
      const response = await fetch(`/api/users?search=${query}`);
      const users = await response.json();
      return users.map(user => ({
        id: user.id,
        name: user.name,
        avatar: user.avatar,
        type: 'user',
        meta: { email: user.email },
      }));
    },
    onSelect: (item) => {
      console.log('Mentioned user:', item);
    },
  },
});

Multiple Entity Types

const mentionPlugin = Mention.extend({
  options: {
    triggers: [
      { char: '@', type: 'user' },
      { char: '#', type: 'channel' },
      { char: '/', type: 'command' },
    ],
    onSearch: async (query, trigger) => {
      const type = trigger.type || 'user';
      const response = await fetch(`/api/${type}s?q=${query}`);
      return response.json();
    },
    onSelect: (item, trigger) => {
      console.log(`Selected ${trigger.type}:`, item);
    },
  },
});
const mentionPlugin = Mention.extend({
  options: {
    char: '[[',
    triggers: [{
      char: '[[',
      type: 'page',
      allowSpaces: true,
    }],
    onSearch: async (query) => {
      const response = await fetch(`/api/pages?search=${query}`);
      const pages = await response.json();
      return pages.map(page => ({
        id: page.id,
        name: page.title,
        type: 'page',
        meta: { path: page.path },
      }));
    },
  },
});

Parsers

HTML

  • Deserialize: <span> with data-mention-id and other data attributes
  • Serialize: Mention → <span> with data attributes for id, name, type, etc.

Markdown

  • Serialize: Mention → [@name](mention:id) format

Email

  • Serialize: <span> with inline styles and mention data
  • Link — Hyperlinks

Build docs developers (and LLMs) love