@ocwilsonchow/editor
v0.1.1
Published
Unstyled mention composer for React 19
Maintainers
Readme
@ocwilsonchow/editor
Unstyled mention composer for React 19. Type @ to insert mentions, Enter to submit, Shift+Enter for a newline.
The package ships no CSS. Style it with className and data-slot in the host app.
Docs: https://editor.slchow.com
Install
npm i @ocwilsonchow/editorPeer dependencies: react and react-dom >=19. TipTap is a hidden runtime dependency — you do not install it yourself.
The package is ESM-only. In Next.js, import it from a Client Component.
import { MentionEditor } from "@ocwilsonchow/editor";Usage
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>items is a resolver. It receives the current query and navigation context, and may return a list or a promise. Use action: "navigate" to drill into a folder. Omit action (or set "insert") to insert { id, label, path }. The chip shows label (the filename). Nested inserts join ancestor labels into path (src/app/page.tsx) for submit text.
import type { MentionItem, MentionItemsResolver } from "@ocwilsonchow/editor";
const items: MentionItemsResolver = (query, { parentId }) => {
const pool: MentionItem[] = parentId
? [{ id: "readme", label: "README" }]
: [
{ id: "docs", label: "Docs", action: "navigate" },
{ id: "alice", label: "Alice" },
];
return pool.filter((item) =>
item.label.toLowerCase().includes(query.toLowerCase()),
);
};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. Use the compound parts when you need a custom list row.
<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>Suggestions owns the portal, caret position, listbox semantics, and keyboard navigation.
Also available on MentionEditor and Root: char (default @), disabled, autoFocus, and className. The preset accepts editorClassName for the content surface. Suggestions accepts className, itemClassName, and loading / empty as ReactNode (the preset maps loadingText / emptyText strings). Extra fields on TItem extends MentionItem pass through to the Suggestions render function.
Submit payload
onSubmit and onChange receive:
{
text: string; // mentions serialized as `${char}${path}`
html: string;
mentions: { id: string; label: string; path: string }[];
}Enter submits only when the suggestion list is closed. While the list is open, Enter inserts or navigates.
A ref exposes focus(), clear(), and getPayload().
const ref = useRef<MentionEditorHandle>(null);
<MentionEditor ref={ref} items={items} onSubmit={onSubmit} />Keyboard
| Key | Action |
| --- | --- |
| @ | Open suggestions (or the char you pass) |
| ↑ / ↓ | Move the active row |
| Enter | Insert or navigate while the list is open; submit when it is closed |
| Shift+Enter | Newline |
| Escape | Close suggestions |
| ← | Go back one level when nested |
Styling
Slots you can target:
| data-slot | Element |
| --- | --- |
| mention-editor | Root wrapper (data-disabled="true" when disabled) |
| mention-editor-content | Content wrapper |
| mention-editor-surface | Contenteditable surface |
| mention-editor-portal | Fixed suggestion portal |
| mention-editor-list | Listbox |
| mention-editor-item | Option row (data-active="true" when highlighted) |
| mention-editor-footer | Footer |
| mention | Inserted mention chip |
