Typing @ opens a popup that filters on the text typed after it. Selecting an item replaces that text with a markable void mention node. The package owns the trigger policy, mention insertion, selection movement and Markdown mention serialization. The registry owns the demo item list and the popup UI.
mention nodes with required ref and optional label.@, through Combobox.[display text](mention:ref) links.MentionKit installs MentionPlugin with the registry mention node, the mention popup, and a trigger rule that allows @ at the start of a line, after whitespace, or after quotes.
'use client';
import { IS_APPLE } from 'platejs';
import { MentionPlugin } from 'platejs/mention/react';
import {
type EditorElementProps,
EditorElement,
useEditorFocused,
useEditorReadOnly,
useElementSelected,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { useMounted } from '@/hooks/use-mounted';
import { inlineSuggestionVariants } from '@/lib/inline-suggestion';
import {
InlineCombobox,
InlineComboboxContent,
InlineComboboxEmpty,
InlineComboboxGroup,
InlineComboboxItem,
} from './inline-combobox';
export function MentionElement(
props: EditorElementProps<typeof MentionPlugin> & {
prefix?: string;
}
) {
const { element } = props;
const selected = useElementSelected();
const focused = useEditorFocused();
const mounted = useMounted();
const readOnly = useEditorReadOnly();
const label = element.label ?? element.ref;
return (
<EditorElement
{...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].bold === true && 'font-bold',
element.children[0].italic === true && 'italic',
element.children[0].underline === true && 'underline'
)}
attributes={{
...props.attributes,
contentEditable: false,
'data-editor-value': label,
draggable: true,
}}
>
{mounted && IS_APPLE ? (
// Mac OS IME https://github.com/ianstormtaylor/slate/issues/3490
<>
{props.children}
{props.prefix}
{label}
</>
) : (
// Others like Android https://github.com/ianstormtaylor/slate/pull/5360
<>
{props.prefix}
{label}
{props.children}
</>
)}
</EditorElement>
);
}
export function MentionCombobox({
editableRef,
}: {
editableRef: React.RefObject<HTMLDivElement | null>;
}) {
return (
<InlineCombobox editableRef={editableRef} plugin={MentionPlugin}>
<InlineComboboxContent className="my-1.5">
<InlineComboboxEmpty>No results</InlineComboboxEmpty>
<InlineComboboxGroup>
{MENTIONABLES.map((item) => (
<InlineComboboxItem
key={item.ref}
value={item.label}
onSelect={(tx) => {
tx.plugin(MentionPlugin).insert({
label: item.label,
ref: item.ref,
});
}}
>
{item.label}
</InlineComboboxItem>
))}
</InlineComboboxGroup>
</InlineComboboxContent>
</InlineCombobox>
);
}
const MENTIONABLES = [
{ ref: '0', label: 'Aayla Secura' },
{ ref: '1', label: 'Adi Gallia' },
{
ref: '2',
label: 'Admiral Dodd Rancit',
},
{
ref: '3',
label: 'Admiral Firmus Piett',
},
{
ref: '4',
label: 'Admiral Gial Ackbar',
},
{ ref: '5', label: 'Admiral Ozzel' },
{ ref: '6', label: 'Admiral Raddus' },
{
ref: '7',
label: 'Admiral Terrinald Screed',
},
{ ref: '8', label: 'Admiral Trench' },
{
ref: '9',
label: 'Admiral U.O. Statura',
},
{ ref: '10', label: 'Agen Kolar' },
{ ref: '11', label: 'Agent Kallus' },
{
ref: '12',
label: 'Aiolin and Morit Astarte',
},
{ ref: '13', label: 'Aks Moe' },
{ ref: '14', label: 'Almec' },
{ ref: '15', label: 'Alton Kastle' },
{ ref: '16', label: 'Amee' },
{ ref: '17', label: 'AP-5' },
{ ref: '18', label: 'Armitage Hux' },
{ ref: '19', label: 'Artoo' },
{ ref: '20', label: 'Arvel Crynyd' },
{ ref: '21', label: 'Asajj Ventress' },
{ ref: '22', label: 'Aurra Sing' },
{ ref: '23', label: 'AZI-3' },
{ ref: '24', label: 'Bala-Tik' },
{ ref: '25', label: 'Barada' },
{ ref: '26', label: 'Bargwill Tomder' },
{ ref: '27', label: 'Baron Papanoida' },
{ ref: '28', label: 'Barriss Offee' },
{ ref: '29', label: 'Baze Malbus' },
{ ref: '30', label: 'Bazine Netal' },
{ ref: '31', label: 'BB-8' },
{ ref: '32', label: 'BB-9E' },
{ ref: '33', label: 'Ben Quadinaros' },
{ ref: '34', label: 'Berch Teller' },
{ ref: '35', label: 'Beru Lars' },
{ ref: '36', label: 'Bib Fortuna' },
{
ref: '37',
label: 'Biggs Darklighter',
},
{ ref: '38', label: 'Black Krrsantan' },
{ ref: '39', label: 'Bo-Katan Kryze' },
{ ref: '40', label: 'Boba Fett' },
{ ref: '41', label: 'Bobbajo' },
{ ref: '42', label: 'Bodhi Rook' },
{ ref: '43', label: 'Borvo the Hutt' },
{ ref: '44', label: 'Boss Nass' },
{ ref: '45', label: 'Bossk' },
{
ref: '46',
label: 'Breha Antilles-Organa',
},
{ ref: '47', label: 'Bren Derlin' },
{ ref: '48', label: 'Brendol Hux' },
{ ref: '49', label: 'BT-1' },
];
export const MentionKit = [
MentionPlugin.configure({
component: MentionElement,
initialState: {
triggerPreviousCharPattern: /^$|^[\s"']$/,
},
slots: { afterEditable: MentionCombobox },
}),
];'use client';
import { IS_APPLE } from 'platejs';
import { MentionPlugin } from 'platejs/mention/react';
import {
type EditorElementProps,
EditorElement,
useEditorFocused,
useEditorReadOnly,
useElementSelected,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { useMounted } from '@/hooks/use-mounted';
import { inlineSuggestionVariants } from '@/lib/inline-suggestion';
import {
import { createEditor } from 'platejs/react';
import { MentionKit } from '@/components/editor/mention';
export const editor = createEditor({
plugins: MentionKit,
});import { createEditor } from 'platejs/react';
import { MentionKit } from '@/components/editor/mention';
export const editor = createEditor({
plugins: MentionKit,
mention renders the mention node and the MentionCombobox popup. The demo data lives in that registry UI file, so replace it with your app users, pages, or records.
'use client';
import { IS_APPLE } from 'platejs';
import { MentionPlugin } from 'platejs/mention/react';
import {
type EditorElementProps,
EditorElement,
useEditorFocused,
useEditorReadOnly,
useElementSelected,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { useMounted } from '@/hooks/use-mounted';
import { inlineSuggestionVariants } from '@/lib/inline-suggestion';
import
mention-static uses BaseMentionPlugin with the static mention node for read-only output.
import { BaseMentionPlugin } from 'platejs/mention';
import { type EditorElementProps, EditorElement } from 'platejs/static';
import * as React from 'react';
import { cn } from '@/lib/utils';
export function MentionElementStatic(
props: EditorElementProps<typeof BaseMentionPlugin> & {
prefix?: string;
}
) {
const { prefix } = props;
const { element } = props;
| Layer | Owner | What It Does |
|---|---|---|
platejs | Package | Exports BaseMentionPlugin and the scoped mention update. |
platejs/react | Package | Exports MentionPlugin. |
platejs/combobox/react | Package | Exports useCombobox, which opens the popup on a typed trigger. |
mention | Registry | Adds MentionPlugin with the mention node and popup. |
mention-static | Registry | Adds BaseMentionPlugin.configure({ component: MentionElementStatic }). |
mention | Registry UI | Renders mention atoms, the popup, and demo items. |
inline-combobox | Registry UI | Renders the Ariakit listbox, groups, items, and empty state at the trigger. |
platejs/markdown | Package | Serializes mentions as mention: links and parses them back. |
Mount the popup in the plugin's afterEditable slot:
import { MentionPlugin } from 'platejs/mention/react';
import { createEditor } from 'platejs/react';
import {
MentionCombobox,
MentionElement,
} from '@/components/editor/mention';
export const editor = createEditor({
plugins: [
MentionPlugin.configure({
component: MentionElement,
initialState: {
triggerPreviousCharPattern: /^$|^[\s"']$/,
},
slots: { afterEditable: MentionCombobox },
}),
],
});Call the mention update from the item's onSelect. The popup first removes the typed @ and query in the same transaction. The update 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.
<InlineComboboxItem
value={item.label}
onSelect={(tx) => {
tx.plugin(MentionPlugin).insert({
label: item.label,
ref: item.ref,
});
}}
>
{item.label}
</InlineComboboxItem><InlineComboboxItem
value={item.label}
onSelect={(tx)
PLUGINS.mention identifies the capability and its default persisted element type is mention.
Reusable code reads the configured type through the plugin portal's schema.type.
const value = [
{
children: [
{ text: 'Assigned to ' },
{
children: [{ text: '' }],
label: 'Jane Smith',
ref: 'user_123',
type: 'mention',
},
{ text: '.' },
],
type: 'paragraph',
},
];const value = [
{
children: [
{ text: 'Assigned to ' },
{
children: [{ text: '' }],
label: 'Jane Smith',
ref: 'user_123',
type: 'mention',
},
{ text: '.' },
],
type: 'paragraph',
},
];| Field | Type | Notes |
|---|---|---|
type | 'mention' | Inline void mention node. |
ref | string | Required persisted association with the mentioned entity. |
label | string | Optional visible text; rendering falls back to ref. |
children | [{ text: '' }] | Empty child required for Plate inline void nodes. |
The mention element declares schema.element.void: 'markable-inline', so marks
on its empty child can style the rendered mention.
The popup opens when the user types @ and the combobox rules allow it.
| Gate | Source |
|---|---|
Typed text matches trigger | string, string[], or RegExp. |
Previous character matches triggerPreviousCharPattern | Defaults to /^\s?$/; registry kit uses /^$|^[\s"']$/. |
triggerQuery(editor) returns true | Optional app veto for custom contexts. |
Until an item is selected, @ and the query are ordinary text in the document. Escape closes the popup and keeps that text.
platejs/markdown writes a mention as a link. The URL is mention: followed by the URL-encoded ref, and the link text is label ?? ref.
Hello [Jane Smith](mention:user_123).Hello [Jane Smith](mention:user_123).Parsing reads mention: links back into mention nodes. Other links such as [@docs](/docs/mention) stay links, and bare @alice text stays text.
| API | Package | Use |
|---|---|---|
BaseMentionPlugin | platejs | Headless inline markable void mention plugin with a ComboboxState trigger policy. |
MentionPlugin | platejs/react | React mention plugin. |
editor.plugin(MentionPlugin).update.insert({ ref, label? }) | BaseMentionPlugin update | Inserts the mention node at the current selection. |
useCombobox | platejs/combobox/react | Opens the popup on a typed @ and completes the query. |
inline-combobox | Copied registry UI | Handles the listbox, keyboard navigation and filtering for the mention popup. |
'use client';
import { IS_APPLE } from 'platejs';
import { MentionPlugin } from 'platejs/mention/react';
import {
type EditorElementProps,
EditorElement,
useEditorFocused,
useEditorReadOnly,
useElementSelected,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { useMounted } from '@/hooks/use-mounted';
import { inlineSuggestionVariants } from '@/lib/inline-suggestion';
import {
InlineCombobox,
InlineComboboxContent,
InlineComboboxEmpty,
InlineComboboxGroup,
InlineComboboxItem,
} from './inline-combobox';
export function MentionElement(
props: EditorElementProps<typeof MentionPlugin> & {
prefix?: string;
}
) {
const { element } = props;
const selected = useElementSelected();
const focused = useEditorFocused();
const mounted = useMounted();
const readOnly = useEditorReadOnly();
const label = element.label ?? element.ref;
return (
<EditorElement
{...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].bold === true && 'font-bold',
element.children[0].italic === true && 'italic',
element.children[0].underline === true && 'underline'
)}
attributes={{
...props.attributes,
contentEditable: false,
'data-editor-value': label,
draggable: true,
}}
>
{mounted && IS_APPLE ? (
// Mac OS IME https://github.com/ianstormtaylor/slate/issues/3490
<>
{props.children}
{props.prefix}
{label}
</>
) : (
// Others like Android https://github.com/ianstormtaylor/slate/pull/5360
<>
{props.prefix}
{label}
{props.children}
</>
)}
</EditorElement>
);
}
export function MentionCombobox({
editableRef,
}: {
editableRef: React.RefObject<HTMLDivElement | null>;
}) {
return (
<InlineCombobox editableRef={editableRef} plugin={MentionPlugin}>
<InlineComboboxContent className="my-1.5">
<InlineComboboxEmpty>No results</InlineComboboxEmpty>
<InlineComboboxGroup>
{MENTIONABLES.map((item) => (
<InlineComboboxItem
key={item.ref}
value={item.label}
onSelect={(tx) => {
tx.plugin(MentionPlugin).insert({
label: item.label,
ref: item.ref,
});
}}
>
{item.label}
</InlineComboboxItem>
))}
</InlineComboboxGroup>
</InlineComboboxContent>
</InlineCombobox>
);
}
const MENTIONABLES = [
{ ref: '0', label: 'Aayla Secura' },
{ ref: '1', label: 'Adi Gallia' },
{
ref: '2',
label: 'Admiral Dodd Rancit',
},
{
ref: '3',
label: 'Admiral Firmus Piett',
},
{
ref: '4',
label: 'Admiral Gial Ackbar',
},
{ ref: '5', label: 'Admiral Ozzel' },
{ ref: '6', label: 'Admiral Raddus' },
{
ref: '7',
label: 'Admiral Terrinald Screed',
},
{ ref: '8', label: 'Admiral Trench' },
{
ref: '9',
label: 'Admiral U.O. Statura',
},
{ ref: '10', label: 'Agen Kolar' },
{ ref: '11', label: 'Agent Kallus' },
{
ref: '12',
label: 'Aiolin and Morit Astarte',
},
{ ref: '13', label: 'Aks Moe' },
{ ref: '14', label: 'Almec' },
{ ref: '15', label: 'Alton Kastle' },
{ ref: '16', label: 'Amee' },
{ ref: '17', label: 'AP-5' },
{ ref: '18', label: 'Armitage Hux' },
{ ref: '19', label: 'Artoo' },
{ ref: '20', label: 'Arvel Crynyd' },
{ ref: '21', label: 'Asajj Ventress' },
{ ref: '22', label: 'Aurra Sing' },
{ ref: '23', label: 'AZI-3' },
{ ref: '24', label: 'Bala-Tik' },
{ ref: '25', label: 'Barada' },
{ ref: '26', label: 'Bargwill Tomder' },
{ ref: '27', label: 'Baron Papanoida' },
{ ref: '28', label: 'Barriss Offee' },
{ ref: '29', label: 'Baze Malbus' },
{ ref: '30', label: 'Bazine Netal' },
{ ref: '31', label: 'BB-8' },
{ ref: '32', label: 'BB-9E' },
{ ref: '33', label: 'Ben Quadinaros' },
{ ref: '34', label: 'Berch Teller' },
{ ref: '35', label: 'Beru Lars' },
{ ref: '36', label: 'Bib Fortuna' },
{
ref: '37',
label: 'Biggs Darklighter',
},
{ ref: '38', label: 'Black Krrsantan' },
{ ref: '39', label: 'Bo-Katan Kryze' },
{ ref: '40', label: 'Boba Fett' },
{ ref: '41', label: 'Bobbajo' },
{ ref: '42', label: 'Bodhi Rook' },
{ ref: '43', label: 'Borvo the Hutt' },
{ ref: '44', label: 'Boss Nass' },
{ ref: '45', label: 'Bossk' },
{
ref: '46',
label: 'Breha Antilles-Organa',
},
{ ref: '47', label: 'Bren Derlin' },
{ ref: '48', label: 'Brendol Hux' },
{ ref: '49', label: 'BT-1' },
];
export const MentionKit = [
MentionPlugin.configure({
component: MentionElement,
initialState: {
triggerPreviousCharPattern: /^$|^[\s"']$/,
},
slots: { afterEditable: MentionCombobox },
}),
];import { BaseMentionPlugin } from 'platejs/mention';
import { type EditorElementProps, EditorElement } from 'platejs/static';
import * as React from 'react';
import { cn } from '@/lib/utils';
export function MentionElementStatic(
props: EditorElementProps<typeof BaseMentionPlugin> & {
prefix?: string;
}
) {
const { prefix } = props;
const { element } = props;
const label = element.label ?? element.ref;
return (
<EditorElement
{...props}
as="span"
className={cn(
'inline-block rounded-md bg-muted px-1.5 py-0.5 align-baseline font-medium text-sm',
element.children[0].bold === true && 'font-bold',
element.children[0].italic === true && 'italic',
element.children[0].underline === true && 'underline'
)}
attributes={{
...props.attributes,
'data-editor-value': label,
}}
>
{props.children}
{prefix}
{label}
</EditorElement>
);
}
export const BaseMentionKit = [
BaseMentionPlugin.configure({
component: MentionElementStatic,
}),
];import { MentionPlugin } from 'platejs/mention/react';
import { createEditor } from 'platejs/react';
import {
MentionCombobox,
MentionElement,
} from '@/components/editor/mention';
export const editor = createEditor({
plugins: [
MentionPlugin.configure({
component: MentionElement,
initialState: {
triggerPreviousCharPattern: /^$|^[\s"']$/,
},
slots: { afterEditable: MentionCombobox },
}),
],
});