@ocwilsonchow/editor is a mention composer: type @ to insert mentions, press Enter to submit, and Shift+Enter for a newline. The package ships no CSS. Style it with className and data-slot.
For the source code, check out the GitHub.
Installation
npm i @ocwilsonchow/editorPeer dependencies: react and react-dom >=19. TipTap is a hidden runtime dependency — you do not install it yourself.
import { MentionEditor } from "@ocwilsonchow/editor";The package is ESM-only. Import it from a Client Component in Next.js.
Preset
MentionEditor is a convenience composition. children render in the footer.
<MentionEditor
items={items}
onSubmit={onSubmit}
placeholder="Write a message"
emptyText="No results"
loadingText="Loading"
backText="Back"
>
<button type="button">Send</button>
</MentionEditor>Example data
{
"root": [
{
"id": "src",
"label": "src",
"action": "navigate",
"hint": "folder"
},
{
"id": "packages",
"label": "packages",
"action": "navigate",
"hint": "folder"
},
{
"id": "content",
"label": "content",
"action": "navigate",
"hint": "folder"
},
{
"id": "readme",
"label": "README.md",
"hint": "md"
},
{
"id": "package-json",
"label": "package.json",
"hint": "json"
},
{
"id": "tsconfig",
"label": "tsconfig.json",
"hint": "json"
},
{
"id": "agents",
"label": "AGENTS.md",
"hint": "md"
}
],
"src": [
{
"id": "src-app",
"label": "app",
"action": "navigate",
"hint": "folder"
},
{
"id": "src-components",
"label": "components",
"action": "navigate",
"hint": "folder"
},
{
"id": "src-lib",
"label": "lib",
"action": "navigate",
"hint": "folder"
},
{
"id": "src-globals",
"label": "globals.css",
"hint": "css"
}
],
"src-app": [
{
"id": "src-app-page",
"label": "page.tsx",
"hint": "tsx"
},
{
"id": "src-app-layout",
"label": "layout.tsx",
"hint": "tsx"
}
],
"src-components": [
{
"id": "src-theme-toggle",
"label": "theme-toggle.tsx",
"hint": "tsx"
},
{
"id": "src-install-command",
"label": "install-command.tsx",
"hint": "tsx"
},
{
"id": "src-code-block",
"label": "code-block.tsx",
"hint": "tsx"
}
],
"src-lib": [
{
"id": "src-mention-items",
"label": "mention-items.ts",
"hint": "ts"
}
],
"packages": [
{
"id": "pkg-editor",
"label": "editor",
"action": "navigate",
"hint": "folder"
}
],
"pkg-editor": [
{
"id": "pkg-editor-src",
"label": "src",
"action": "navigate",
"hint": "folder"
},
{
"id": "pkg-editor-package",
"label": "package.json",
"hint": "json"
},
{
"id": "pkg-editor-tsup",
"label": "tsup.config.ts",
"hint": "ts"
}
],
"pkg-editor-src": [
{
"id": "pkg-editor-index",
"label": "index.ts",
"hint": "ts"
},
{
"id": "pkg-editor-root",
"label": "root.tsx",
"hint": "tsx"
},
{
"id": "pkg-editor-types",
"label": "types.ts",
"hint": "ts"
}
],
"content": [
{
"id": "content-docs",
"label": "docs",
"action": "navigate",
"hint": "folder"
}
],
"content-docs": [
{
"id": "docs-index",
"label": "index.md",
"hint": "md"
},
{
"id": "docs-install",
"label": "installation.md",
"hint": "md"
},
{
"id": "docs-preset",
"label": "preset.mdx",
"hint": "mdx"
}
]
}items receives the query and navigation context. Use action: "navigate" to drill in, or omit it to insert { id, label }.
items can return a promise. The list shows loadingText until it resolves.
const items: MentionItemsResolver = async (query, { parentId }) => {
const params = new URLSearchParams({ q: query });
if (parentId) {
params.set("parentId", parentId);
}
const res = await fetch(`/api/mentions?${params}`);
return res.json();
};The preset composes Root, Content, Suggestions, and Footer for the common case. Use the compound parts when you need a custom list row.
Primitives
The public compound interface is four parts. Suggestions owns the portal, caret position, listbox semantics, and navigation.
<MentionEditor.Root items={items} onSubmit={onSubmit}>
<MentionEditor.Content placeholder="Write a message" />
<MentionEditor.Suggestions
empty={<span>Nothing here</span>}
back={({ path }) => <span>Back from {path.at(-1)?.label}</span>}
>
{({ item, active }) => (
<span data-active={active}>{item.label}</span>
)}
</MentionEditor.Suggestions>
<MentionEditor.Footer>
<button type="button">Send</button>
</MentionEditor.Footer>
</MentionEditor.Root>Extra fields on TItem extends MentionItem pass through to the Suggestions render function. Also available on MentionEditor and Root: char (default @), disabled, autoFocus, and className.
The playground below uses the compound API with custom suggestion rows.
Submit payload
onSubmit and onChange receive:
{
text: string; // mentions serialized as `${char}${label}`
html: string;
mentions: { id: string; label: string }[];
}Enter submits only when the suggestion list is closed. A ref exposes focus(), clear(), and getPayload().
const ref = useRef<MentionEditorHandle>(null);
<MentionEditor ref={ref} items={items} onSubmit={onSubmit} />Styling
Style slots with className or data-slot: mention-editor, mention-editor-content, mention-editor-surface, mention-editor-portal, mention-editor-list, mention-editor-item (data-active when highlighted), mention-editor-footer, and mention.