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-mentionand injects asuggestionconfiguration that delegates rendering to a React NodeView viarenderNodeViewClosure.src/utils/renderNodeView.ts: Contains therenderNodeViewClosurehelper, which creates aReactRendererfrom@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 theselectedIndexstate, handles arrow key navigation, and invokes thecommandcallback 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.tswraps@tiptap/extension-mentionto inject React-based rendering. - The
renderNodeViewClosureutility bridges Tiptap’s imperative API with React components, positioning the popup at the cursor coordinates. - Supply a custom user list by implementing the
itemsfunction in thesuggestionconfiguration; it receives the typed query and returns filtered results. NodeViewMentionListhandles all UI interactions including keyboard navigation, mouse selection, and the finalcommandinvocation to insert the mention node.- Support for multiple trigger characters is available via the
suggestionsarray 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →