How to Implement @Mentions with Custom User Lists in the React Tiptap Editor

To implement @mentions with a custom user list in the React Tiptap editor, configure the Mention extension with a suggestion object that defines the trigger character and an items function to filter your user array, which the library then renders through a React NodeView popup powered by NodeViewMentionList.

The hunghg255/reactjs-tiptap-editor repository provides a React-centric wrapper around the official @tiptap/extension-mention that replaces the default suggestion UI with a fully customizable React component. This approach allows you to inject any user list—static or dynamic—into a typeahead popup that integrates seamlessly with the editor's cursor position and keyboard navigation.

Architecture of the Mention System

The implementation relies on a thin wrapper that bridges Tiptap’s vanilla JavaScript extension system with React’s component tree.

  • src/extensions/Mention/Mention.ts: Extends @tiptap/extension-mention and injects a suggestion configuration that delegates rendering to a React NodeView via renderNodeViewClosure.
  • src/utils/renderNodeView.ts: Contains the renderNodeViewClosure helper, which creates a ReactRenderer from @tiptap/react. This mounts the suggestion list as an absolutely positioned element and syncs it with the editor’s cursor rectangle while forwarding keyboard events.
  • src/extensions/Mention/components/NodeViewMentionList.tsx: The React component that renders the filtered user list, manages the selectedIndex state, handles arrow key navigation, and invokes the command callback upon selection to insert the mention node into the document.

When a user types the trigger character @, Tiptap invokes the items function defined in your configuration. The returned array is passed to NodeViewMentionList, which displays the popup and handles the insertion command.

Configuring @Mentions with a Custom User List

1. Define Your User Data Structure

Create an array of user objects that match the expected schema. The playground example in playground/src/App.tsx demonstrates this structure:

const MOCK_USERS = [
  { id: '0', label: 'alice', avatar: { src: 'https://example.com/alice.png' } },
  { id: '1', label: 'bob', avatar: { src: 'https://example.com/bob.png' } },
  { id: '2', label: 'charlie', avatar: { src: 'https://example.com/charlie.png' } },
];

Each object requires at minimum an id and label property. The avatar field is optional and used by the default NodeViewMentionList component for rendering thumbnails.

2. Configure the Mention Extension

Import the wrapper from the library and supply a suggestion (or suggestions) configuration. In playground/src/App.tsx, the extension is configured to filter the MOCK_USERS array based on the query string:

import { Mention } from 'reactjs-tiptap-editor/mention';

const extensions = [
  Mention.configure({
    suggestion: {
      char: '@',
      items: async ({ query }) => {
        return MOCK_USERS.filter((user) =>
          user.label.toLowerCase().startsWith(query.toLowerCase())
        );
      },
    },
  }),
];

The items function receives an object containing the typed query and must return a promise or an array of items. The wrapper forwards these items to NodeViewMentionList, which renders the popup and manages selection state.

3. Initialize the Editor

Pass the configured extensions to the useEditor hook as standard:

import { useEditor } from '@tiptap/react';

const editor = useEditor({
  content: '<p>Try typing @...</p>',
  extensions,
});

The mention functionality activates immediately when the trigger character is detected.

Advanced Configuration Options

Supporting Multiple Trigger Characters

The wrapper supports an array of suggestion sources via the suggestions property (plural). This allows you to implement @ for users and # for tags within the same extension instance:

Mention.configure({
  suggestions: [
    {
      char: '@',
      items: async ({ query }) => fetchUsers(query),
    },
    {
      char: '#',
      items: async ({ query }) => fetchTags(query),
    },
  ],
});

Each entry in the suggestions array spawns a separate NodeViewMentionList instance, isolated by trigger character.

Customizing the Popup Appearance

The default styling resides in src/styles/mention.scss. Override these CSS custom properties or classes in your own stylesheet to match your design system:

.mention-suggestion {
  background: var(--richtext-popover-bg);
  border: 1px solid var(--richtext-border);
  border-radius: 6px;
  
  .mention-item {
    padding: 8px 12px;
    
    &.is-selected {
      background-color: var(--richtext-accent);
    }
  }
}

The NodeViewMentionList component automatically applies the is-selected class to the active entry and scrolls it into view during keyboard navigation.

Summary

  • The Mention extension in src/extensions/Mention/Mention.ts wraps @tiptap/extension-mention to inject React-based rendering.
  • The renderNodeViewClosure utility bridges Tiptap’s imperative API with React components, positioning the popup at the cursor coordinates.
  • Supply a custom user list by implementing the items function in the suggestion configuration; it receives the typed query and returns filtered results.
  • NodeViewMentionList handles all UI interactions including keyboard navigation, mouse selection, and the final command invocation to insert the mention node.
  • Support for multiple trigger characters is available via the suggestions array configuration.

Frequently Asked Questions

How do I filter the user list based on the typed query?

The items function in your suggestion configuration receives an object with a query property containing the text typed after the trigger character. Implement your filtering logic—case-insensitive prefix matching, fuzzy search, or API calls—inside this function and return the filtered array. The playground example in playground/src/App.tsx demonstrates simple case-insensitive prefix filtering against a static MOCK_USERS array.

Can I use multiple trigger characters for different entity types?

Yes. Instead of the singular suggestion key, use the suggestions array (plural) in your configuration. Each element in the array defines its own char (trigger character) and items function, allowing you to implement separate workflows for users (@), tags (#), or any other entity type. The wrapper instantiates a separate NodeViewMentionList for each trigger automatically.

How do I customize the appearance of the mention nodes and suggestion popup?

The suggestion popup styling is controlled via src/styles/mention.scss in the source repository. You can override the default CSS classes—such as .mention-suggestion for the container and .mention-item for list entries—in your application's global styles. To customize the rendered mention node inside the document, modify the HTMLAttributes in the Mention extension configuration or override the renderHTML method in a custom extension.

Is it possible to fetch users asynchronously from an API?

Absolutely. The items function supports asynchronous operations. Return a Promise that resolves to your user array, or use async/await syntax to fetch data from an external API based on the query parameter. The ReactRenderer will wait for the promise to resolve before updating NodeViewMentionList with the new items, automatically handling loading states if you implement them within your custom list component.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →