MentionNode
Inline mention chip for @users,
MentionNode is a DecoratorNode that renders as a non-editable inline chip inside the editor. It supports any trigger character (@, #, or custom) and stores the referenced entity's ID, label, and trigger for serialization.
Import
import {
MentionNode,
$createMentionNode,
$isMentionNode,
} from "@blokhaus/core";
import type { MentionPayload } from "@blokhaus/core";MentionPayload
interface MentionPayload {
id: string;
label: string;
trigger: string;
key?: NodeKey;
}| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier of the mentioned entity (user ID, tag ID, etc.). |
label | string | Yes | Display text for the mention chip (e.g., "Alice", "feature-request"). |
trigger | string | Yes | The trigger character that created this mention ("@", "#", etc.). |
key | NodeKey | No | Explicit Lexical node key. |
Functions
$createMentionNode(payload: MentionPayload): MentionNode
Creates a new MentionNode. Must be called inside editor.update(). Typically invoked by a MentionProvider's onSelect callback.
import { $createMentionNode } from "@blokhaus/core";
// Inside a MentionProvider's onSelect:
const onSelect = (item: MentionItem) => {
return $createMentionNode({
id: item.id,
label: item.label,
trigger: "@",
});
};$isMentionNode(node: LexicalNode | null | undefined): node is MentionNode
Type guard that returns true if the given node is a MentionNode.
editor.read(() => {
const root = $getRoot();
const mentions: MentionNode[] = [];
function collectMentions(node: LexicalNode) {
if ($isMentionNode(node)) {
mentions.push(node);
}
if ($isElementNode(node)) {
node.getChildren().forEach(collectMentions);
}
}
root.getChildren().forEach(collectMentions);
console.log(
"Found mentions:",
mentions.map((m) => m.getTextContent()),
);
});Instance methods
| Method | Returns | Description |
|---|---|---|
isInline() | true | Always returns true. Mention nodes are inline elements. |
getTextContent() | string | Returns trigger + label (e.g., "@Alice"). |
Serialized format
The JSON serialization uses mentionId (not id) to avoid collisions with Lexical's internal id field:
type SerializedMentionNode = {
type: "mention";
version: 1;
mentionId: string;
label: string;
trigger: string;
};DOM behavior
- exportDOM: Produces a
<span>withdata-mention-idanddata-mention-triggerattributes, and text content oftrigger + label. - importDOM: Converts pasted
<span>elements that have adata-mention-idattribute back intoMentionNodeinstances.
Keyboard behavior
The mention chip is focusable but not editable. When the node is selected (via Lexical's node selection):
- Pressing
Backspaceremoves the mention from the AST. - Pressing
Deleteremoves the mention from the AST.
Visual styling
The chip color is determined by the trigger character:
#triggers use the--blokhaus-accenttoken (blue by default).- All other triggers (including
@) use the--blokhaus-ai-streamtoken.
The chip renders with a subtle background tint derived from the accent color using CSS color-mix().
Usage with MentionPlugin
MentionNode is typically created by MentionPlugin based on MentionProvider configuration:
"use client";
import { EditorRoot, MentionPlugin, $createMentionNode } from "@blokhaus/core";
import type { MentionProvider, MentionItem } from "@blokhaus/core";
const userProvider: MentionProvider = {
trigger: "@",
onSearch: async (query) => {
const res = await fetch(`/api/users?q=${query}`);
return res.json();
},
renderItem: (item) => (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span>{item.label}</span>
<span style={{ opacity: 0.5 }}>{item.meta}</span>
</div>
),
onSelect: (item) =>
$createMentionNode({ id: item.id, label: item.label, trigger: "@" }),
};
export default function EditorPage() {
return (
<EditorRoot namespace="my-editor">
<MentionPlugin providers={[userProvider]} />
</EditorRoot>
);
}Related
- MentionPlugin -- Plugin that manages the mention dropdown
- Mentions guide -- Full tutorial with async data resolution
- MentionProvider type -- The provider interface