Mention turns trigger text such as @ into an inline combobox input and inserts a markable void mention node when the user selects an item. The package owns trigger detection, input node creation, mention insertion, selection movement, and Markdown mention serialization. The registry owns the demo item list and the inline combobox UI.
Features
- Inline void
mentionnodes withvalueand optionalkey. - Inline void
mention_inputnodes created by trigger text. - Trigger combobox support from
@platejs/combobox. - Configurable trigger strings, regexes, and trigger queries.
- Markable void rendering so bold, italic, and underline can style a mention.
- Optional trailing space after selected mention items.
- Markdown format through
[display text](mention:id)plus bare@namedeserialization.
Fast Path
Add The Kit
MentionKit installs MentionPlugin, MentionInputPlugin, the registry mention nodes, and a trigger rule that allows @ at the start of a line, after whitespace, or after quotes.
'use client';
import { MentionInputPlugin, MentionPlugin } from '@platejs/mention/react';
import {
MentionElement,
MentionInputElement,
} from '@/components/ui/mention-node';
export const MentionKit = [
MentionPlugin.configure({
options: {
triggerPreviousCharPattern: /^$|^[\s"']$/,
},
}).withComponent(MentionElement),
MentionInputPlugin.withComponent(MentionInputElement),
];'use client';
import { MentionInputPlugin, MentionPlugin } from '@platejs/mention/react';
import {
MentionElement,
MentionInputElement,
} from '@/components/ui/mention-node';
export const MentionKit = [
MentionPlugin.configure({
options: {
triggerPreviousCharPattern: /^$|^[\s"']$/,
},
}).withComponent(MentionElement),
MentionInputPlugin.withComponent(MentionInputElement),
];import { createPlateEditor } from 'platejs/react';
import { MentionKit } from '@/components/editor/plugins/mention-kit';
export const editor = createPlateEditor({
plugins: MentionKit,
});import { createPlateEditor } from 'platejs/react';
import { MentionKit } from '@/components/editor/plugins/mention-kit';
export const editor = createPlateEditor({
plugins: MentionKit,
});Render Mentions
mention-node renders both the selected mention and the temporary combobox input. The demo data lives in that registry UI file, so replace it with your app users, pages, or records.
'use client';
import * as React from 'react';
import type { TComboboxInputElement, TMentionElement } from 'platejs';
import type { PlateElementProps } from 'platejs/react';
import { getMentionOnSelectItem } from '@platejs/mention';
import { IS_APPLE, KEYS } from 'platejs';
import {
PlateElement,
useFocused,
useReadOnly,
useSelected,
} from 'platejs/react';
import { cn } from '@/lib/utils';
import { useMounted } from '@/hooks/use-mounted';
import { inlineSuggestionVariants } from '@/lib/suggestion';
import {
InlineCombobox,
InlineComboboxContent,
InlineComboboxEmpty,
InlineComboboxGroup,
InlineComboboxInput,
InlineComboboxItem,
} from './inline-combobox';
export function MentionElement(
props: PlateElementProps<TMentionElement> & {
prefix?: string;
}
) {
const { element } = props;
const selected = useSelected();
const focused = useFocused();
const mounted = useMounted();
const readOnly = useReadOnly();
return (
<PlateElement
{...props}
className={cn(
'inline-block rounded-md bg-muted px-1.5 py-0.5 align-baseline font-medium text-sm',
inlineSuggestionVariants(),
!readOnly && 'cursor-pointer',
selected && focused && 'ring-2 ring-ring',
element.children[0][KEYS.bold] === true && 'font-bold',
element.children[0][KEYS.italic] === true && 'italic',
element.children[0][KEYS.underline] === true && 'underline'
)}
attributes={{
...props.attributes,
contentEditable: false,
'data-slate-value': element.value,
draggable: true,
}}
>
{mounted && IS_APPLE ? (
// Mac OS IME https://github.com/ianstormtaylor/slate/issues/3490
<>
{props.children}
{props.prefix}
{element.value}
</>
) : (
// Others like Android https://github.com/ianstormtaylor/slate/pull/5360
<>
{props.prefix}
{element.value}
{props.children}
</>
)}
</PlateElement>
);
}
const onSelectItem = getMentionOnSelectItem();
export function MentionInputElement(
props: PlateElementProps<TComboboxInputElement>
) {
const { editor, element } = props;
const [search, setSearch] = React.useState('');
return (
<PlateElement {...props} as="span">
<InlineCombobox
value={search}
element={element}
setValue={setSearch}
showTrigger={false}
trigger="@"
>
<span className="inline-block rounded-md bg-muted px-1.5 py-0.5 align-baseline text-sm ring-ring focus-within:ring-2">
<InlineComboboxInput />
</span>
<InlineComboboxContent className="my-1.5">
<InlineComboboxEmpty>No results</InlineComboboxEmpty>
<InlineComboboxGroup>
{MENTIONABLES.map((item) => (
<InlineComboboxItem
key={item.key}
value={item.text}
onClick={() => onSelectItem(editor, item, search)}
>
{item.text}
</InlineComboboxItem>
))}
</InlineComboboxGroup>
</InlineComboboxContent>
</InlineCombobox>
{props.children}
</PlateElement>
);
}
const MENTIONABLES = [
{ key: '0', text: 'Aayla Secura' },
{ key: '1', text: 'Adi Gallia' },
{
key: '2',
text: 'Admiral Dodd Rancit',
},
{
key: '3',
text: 'Admiral Firmus Piett',
},
{
key: '4',
text: 'Admiral Gial Ackbar',
},
{ key: '5', text: 'Admiral Ozzel' },
{ key: '6', text: 'Admiral Raddus' },
{
key: '7',
text: 'Admiral Terrinald Screed',
},
{ key: '8', text: 'Admiral Trench' },
{
key: '9',
text: 'Admiral U.O. Statura',
},
{ key: '10', text: 'Agen Kolar' },
{ key: '11', text: 'Agent Kallus' },
{
key: '12',
text: 'Aiolin and Morit Astarte',
},
{ key: '13', text: 'Aks Moe' },
{ key: '14', text: 'Almec' },
{ key: '15', text: 'Alton Kastle' },
{ key: '16', text: 'Amee' },
{ key: '17', text: 'AP-5' },
{ key: '18', text: 'Armitage Hux' },
{ key: '19', text: 'Artoo' },
{ key: '20', text: 'Arvel Crynyd' },
{ key: '21', text: 'Asajj Ventress' },
{ key: '22', text: 'Aurra Sing' },
{ key: '23', text: 'AZI-3' },
{ key: '24', text: 'Bala-Tik' },
{ key: '25', text: 'Barada' },
{ key: '26', text: 'Bargwill Tomder' },
{ key: '27', text: 'Baron Papanoida' },
{ key: '28', text: 'Barriss Offee' },
{ key: '29', text: 'Baze Malbus' },
{ key: '30', text: 'Bazine Netal' },
{ key: '31', text: 'BB-8' },
{ key: '32', text: 'BB-9E' },
{ key: '33', text: 'Ben Quadinaros' },
{ key: '34', text: 'Berch Teller' },
{ key: '35', text: 'Beru Lars' },
{ key: '36', text: 'Bib Fortuna' },
{
key: '37',
text: 'Biggs Darklighter',
},
{ key: '38', text: 'Black Krrsantan' },
{ key: '39', text: 'Bo-Katan Kryze' },
{ key: '40', text: 'Boba Fett' },
{ key: '41', text: 'Bobbajo' },
{ key: '42', text: 'Bodhi Rook' },
{ key: '43', text: 'Borvo the Hutt' },
{ key: '44', text: 'Boss Nass' },
{ key: '45', text: 'Bossk' },
{
key: '46',
text: 'Breha Antilles-Organa',
},
{ key: '47', text: 'Bren Derlin' },
{ key: '48', text: 'Brendol Hux' },
{ key: '49', text: 'BT-1' },
];'use client';
import * as React from 'react';
import type { TComboboxInputElement, TMentionElement } from 'platejs';
import type { PlateElementProps } from 'platejs/react';
import { getMentionOnSelectItem } from '@platejs/mention';
import { IS_APPLE, KEYS } from 'platejs';
import {
PlateElement,
useFocused,
useReadOnly,
useSelected,
} from 'platejs/react';
import { cn } from '@/lib/utils';
import { useMounted } from '@/hooks/use-mounted';
import { inlineSuggestionVariants } from '@/lib/suggestion';
import {
InlineCombobox,
InlineComboboxContent,
InlineComboboxEmpty,
InlineComboboxGroup,
InlineComboboxInput,
InlineComboboxItem,
} from './inline-combobox';
export function MentionElement(
props: PlateElementProps<TMentionElement> & {
prefix?: string;
}
) {
const { element } = props;
const selected = useSelected();
const focused = useFocused();
const mounted = useMounted();
const readOnly = useReadOnly();
return (
<PlateElement
{...props}
className={cn(
'inline-block rounded-md bg-muted px-1.5 py-0.5 align-baseline font-medium text-sm',
inlineSuggestionVariants(),
!readOnly && 'cursor-pointer',
selected && focused && 'ring-2 ring-ring',
element.children[0][KEYS.bold] === true && 'font-bold',
element.children[0][KEYS.italic] === true && 'italic',
element.children[0][KEYS.underline] === true && 'underline'
)}
attributes={{
...props.attributes,
contentEditable: false,
'data-slate-value': element.value,
draggable: true,
}}
>
{mounted && IS_APPLE ? (
// Mac OS IME https://github.com/ianstormtaylor/slate/issues/3490
<>
{props.children}
{props.prefix}
{element.value}
</>
) : (
// Others like Android https://github.com/ianstormtaylor/slate/pull/5360
<>
{props.prefix}
{element.value}
{props.children}
</>
)}
</PlateElement>
);
}
const onSelectItem = getMentionOnSelectItem();
export function MentionInputElement(
props: PlateElementProps<TComboboxInputElement>
) {
const { editor, element } = props;
const [search, setSearch] = React.useState('');
return (
<PlateElement {...props} as="span">
<InlineCombobox
value={search}
element={element}
setValue={setSearch}
showTrigger={false}
trigger="@"
>
<span className="inline-block rounded-md bg-muted px-1.5 py-0.5 align-baseline text-sm ring-ring focus-within:ring-2">
<InlineComboboxInput />
</span>
<InlineComboboxContent className="my-1.5">
<InlineComboboxEmpty>No results</InlineComboboxEmpty>
<InlineComboboxGroup>
{MENTIONABLES.map((item) => (
<InlineComboboxItem
key={item.key}
value={item.text}
onClick={() => onSelectItem(editor, item, search)}
>
{item.text}
</InlineComboboxItem>
))}
</InlineComboboxGroup>
</InlineComboboxContent>
</InlineCombobox>
{props.children}
</PlateElement>
);
}
const MENTIONABLES = [
{ key: '0', text: 'Aayla Secura' },
{ key: '1', text: 'Adi Gallia' },
{
key: '2',
text: 'Admiral Dodd Rancit',
},
{
key: '3',
text: 'Admiral Firmus Piett',
},
{
key: '4',
text: 'Admiral Gial Ackbar',
},
{ key: '5', text: 'Admiral Ozzel' },
{ key: '6', text: 'Admiral Raddus' },
{
key: '7',
text: 'Admiral Terrinald Screed',
},
{ key: '8', text: 'Admiral Trench' },
{
key: '9',
text: 'Admiral U.O. Statura',
},
{ key: '10', text: 'Agen Kolar' },
{ key: '11', text: 'Agent Kallus' },
{
key: '12',
text: 'Aiolin and Morit Astarte',
},
{ key: '13', text: 'Aks Moe' },
{ key: '14', text: 'Almec' },
{ key: '15', text: 'Alton Kastle' },
{ key: '16', text: 'Amee' },
{ key: '17', text: 'AP-5' },
{ key: '18', text: 'Armitage Hux' },
{ key: '19', text: 'Artoo' },
{ key: '20', text: 'Arvel Crynyd' },
{ key: '21', text: 'Asajj Ventress' },
{ key: '22', text: 'Aurra Sing' },
{ key: '23', text: 'AZI-3' },
{ key: '24', text: 'Bala-Tik' },
{ key: '25', text: 'Barada' },
{ key: '26', text: 'Bargwill Tomder' },
{ key: '27', text: 'Baron Papanoida' },
{ key: '28', text: 'Barriss Offee' },
{ key: '29', text: 'Baze Malbus' },
{ key: '30', text: 'Bazine Netal' },
{ key: '31', text: 'BB-8' },
{ key: '32', text: 'BB-9E' },
{ key: '33', text: 'Ben Quadinaros' },
{ key: '34', text: 'Berch Teller' },
{ key: '35', text: 'Beru Lars' },
{ key: '36', text: 'Bib Fortuna' },
{
key: '37',
text: 'Biggs Darklighter',
},
{ key: '38', text: 'Black Krrsantan' },
{ key: '39', text: 'Bo-Katan Kryze' },
{ key: '40', text: 'Boba Fett' },
{ key: '41', text: 'Bobbajo' },
{ key: '42', text: 'Bodhi Rook' },
{ key: '43', text: 'Borvo the Hutt' },
{ key: '44', text: 'Boss Nass' },
{ key: '45', text: 'Bossk' },
{
key: '46',
text: 'Breha Antilles-Organa',
},
{ key: '47', text: 'Bren Derlin' },
{ key: '48', text: 'Brendol Hux' },
{ key: '49', text: 'BT-1' },
];Add Static Rendering
mention-base-kit uses BaseMentionPlugin with the static mention node for read-only output.
import { BaseMentionPlugin } from '@platejs/mention';
import { MentionElementStatic } from '@/components/ui/mention-node-static';
export const BaseMentionKit = [
BaseMentionPlugin.withComponent(MentionElementStatic),
];import { BaseMentionPlugin } from '@platejs/mention';
import { MentionElementStatic } from '@/components/ui/mention-node-static';
export const BaseMentionKit = [
BaseMentionPlugin.withComponent(MentionElementStatic),
];Ownership
| Layer | Owner | What It Does |
|---|---|---|
@platejs/mention | Package | Exports BaseMentionPlugin, BaseMentionInputPlugin, and getMentionOnSelectItem. |
@platejs/mention/react | Package | Exports MentionPlugin and MentionInputPlugin. |
@platejs/combobox | Package | Provides withTriggerCombobox, trigger options, and combobox input hooks. |
mention-kit | Registry | Adds mention plugins with editable mention and input components. |
mention-base-kit | Registry | Adds BaseMentionPlugin.withComponent(MentionElementStatic). |
mention-node | Registry UI | Renders mention atoms, the inline combobox input, and demo items. |
inline-combobox | Registry UI | Renders the Ariakit-backed popover, input, groups, items, and empty state. |
@platejs/markdown | Package | Serializes mentions as mention: links and deserializes mention links or bare mentions. |
Manual Setup
Install Package
pnpm add @platejs/mentionpnpm add @platejs/mentionAdd Plugins
Configure the mention plugin with the trigger behavior and render both node types.
import { MentionInputPlugin, MentionPlugin } from '@platejs/mention/react';
import { createPlateEditor } from 'platejs/react';
import {
MentionElement,
MentionInputElement,
} from '@/components/ui/mention-node';
export const editor = createPlateEditor({
plugins: [
MentionPlugin.configure({
options: {
triggerPreviousCharPattern: /^$|^[\s"']$/,
},
}).withComponent(MentionElement),
MentionInputPlugin.withComponent(MentionInputElement),
],
});import { MentionInputPlugin, MentionPlugin } from '@platejs/mention/react';
import { createPlateEditor } from 'platejs/react';
import {
MentionElement,
MentionInputElement,
} from '@/components/ui/mention-node';
export const editor = createPlateEditor({
plugins: [
MentionPlugin.configure({
options: {
triggerPreviousCharPattern: /^$|^[\s"']$/,
},
}).withComponent(MentionElement),
MentionInputPlugin.withComponent(MentionInputElement),
],
});Select An Item
Use getMentionOnSelectItem from your combobox item renderer. It inserts the mention, moves the cursor after it, and inserts a trailing space only when insertSpaceAfterMention is enabled and the mention lands at the end of the block.
import { getMentionOnSelectItem } from '@platejs/mention';
const onSelectItem = getMentionOnSelectItem();
<InlineComboboxItem
value={item.text}
onClick={() => onSelectItem(editor, item, search)}
>
{item.text}
</InlineComboboxItem>;import { getMentionOnSelectItem } from '@platejs/mention';
const onSelectItem = getMentionOnSelectItem();
<InlineComboboxItem
value={item.text}
onClick={() => onSelectItem(editor, item, search)}
>
{item.text}
</InlineComboboxItem>;Value Shape
BaseMentionPlugin uses KEYS.mention, which resolves to the mention node type. The input plugin uses KEYS.mentionInput, which resolves to mention_input.
const value = [
{
children: [
{ text: 'Assigned to ' },
{
children: [{ text: '' }],
key: 'user_123',
type: 'mention',
value: 'Jane Smith',
},
{ text: '.' },
],
type: 'p',
},
];const value = [
{
children: [
{ text: 'Assigned to ' },
{
children: [{ text: '' }],
key: 'user_123',
type: 'mention',
value: 'Jane Smith',
},
{ text: '.' },
],
type: 'p',
},
];| Field | Type | Notes |
|---|---|---|
type | 'mention' | Inline void mention node. |
value | string | Display text rendered by the registry node. |
key | unknown | Optional stable id used by selection handlers and Markdown serialization. |
children | [{ text: '' }] | Empty child required for Slate inline void nodes. |
The mention node is isMarkableVoid, so marks on its empty child can style the rendered mention.
Trigger Flow
withTriggerCombobox overrides insertText. It creates a combobox input only when every gate passes.
| Gate | Source |
|---|---|
Inserted text matches trigger | string, string[], or RegExp. |
Insert is not using options.at | Programmatic text insertion bypasses the trigger. |
| Editor has a selection | No selection means no inline input target. |
triggerQuery(editor) returns true | Optional app veto for custom contexts. |
Previous character matches triggerPreviousCharPattern | Defaults to /^\s?$/; registry kit uses /^$|^[\s"']$/. |
The default createComboboxInput creates:
{
children: [{ text: '' }],
trigger: '@',
type: KEYS.mentionInput,
}{
children: [{ text: '' }],
trigger: '@',
type: KEYS.mentionInput,
}If editor.meta.userId exists, the combobox input stores that userId so only the creator sees the transient input in collaborative editors.
Markdown
@platejs/markdown serializes mentions as link-style mention: URLs. It uses key for the URL when present and value for the visible text.
Hello [Jane Smith](mention:user_123).Hello [Jane Smith](mention:user_123).Deserialization supports link-style mentions and bare @alice text. Normal links such as [@docs](/docs/mention) stay links.
API Reference
| API | Package | Use |
|---|---|---|
BaseMentionPlugin | @platejs/mention | Headless inline markable void mention plugin with trigger-combobox behavior. |
BaseMentionInputPlugin | @platejs/mention | Inline void input node inserted while the combobox is active. |
MentionPlugin | @platejs/mention/react | React mention plugin. |
MentionInputPlugin | @platejs/mention/react | React mention input plugin. |
editor.tf.insert.mention({ key, value }) | BaseMentionPlugin transform | Inserts the mention node at the current selection. |
getMentionOnSelectItem({ key? }) | @platejs/mention | Returns an item handler for mention combobox selection. |
withTriggerCombobox | @platejs/combobox | Creates temporary combobox input nodes from trigger text. |
useComboboxInput | @platejs/combobox/react | Handles focus, cancellation, arrow/backspace/escape behavior, and undo/redo forwarding for custom input UI. |