@mdxeditor/typeahead-plugin
v0.4.0
Published
MDXEditor plugin for typeahead functionality
Maintainers
Readme
@mdxeditor/typeahead-plugin
A flexible typeahead/autocomplete plugin for MDXEditor that supports multiple simultaneous autocomplete features (mentions, hashtags, custom triggers) with full styling customization.
Installation
npm install @mdxeditor/typeahead-plugin
# or
pnpm add @mdxeditor/typeahead-plugin
# or
yarn add @mdxeditor/typeahead-pluginFeatures
- Multiple typeahead types - Configure @mentions, #hashtags, :emoji, or any custom trigger
- Style-agnostic - No forced styles, bring your own CSS or component library
- Flexible rendering - Use default rendering, custom CSS classes, or custom render functions
- Async-capable - Search callbacks return Promises for async data fetching
Usage
Basic Example with Mentions
import { MDXEditor } from "@mdxeditor/editor";
import { typeaheadPlugin } from "@mdxeditor/typeahead-plugin";
const users = ["Alice", "Bob", "Charlie", "David"];
function App() {
return (
<MDXEditor
markdown="# Hello World"
plugins={[
typeaheadPlugin({
configs: [
{
type: "mention",
trigger: "@",
searchCallback: async (query) => {
return users.filter((user) =>
user.toLowerCase().includes(query.toLowerCase()),
);
},
renderMenuItem: (user) => <span>@{user}</span>,
},
],
}),
]}
/>
);
}Multiple Typeahead Types
import { MDXEditor } from "@mdxeditor/editor";
import { typeaheadPlugin } from "@mdxeditor/typeahead-plugin";
const users = ["Alice", "Bob", "Charlie"];
const tags = ["important", "urgent", "feature", "bug"];
const emojis = ["smile", "heart", "rocket", "fire"];
function App() {
return (
<MDXEditor
markdown="# Hello World"
plugins={[
typeaheadPlugin({
configs: [
{
type: "mention",
trigger: "@",
searchCallback: async (query) => {
return users.filter((user) =>
user.toLowerCase().includes(query.toLowerCase()),
);
},
renderMenuItem: (user) => (
<div style={{ padding: "4px 8px" }}>
<strong>@{user}</strong>
</div>
),
maxResults: 5,
},
{
type: "hashtag",
trigger: "#",
searchCallback: async (query) => {
return tags.filter((tag) =>
tag.toLowerCase().includes(query.toLowerCase()),
);
},
renderMenuItem: (tag) => (
<div style={{ padding: "4px 8px" }}>#{tag}</div>
),
className: "hashtag-node",
},
{
type: "emoji",
trigger: ":",
searchCallback: async (query) => {
return emojis.filter((emoji) =>
emoji.toLowerCase().includes(query.toLowerCase()),
);
},
renderMenuItem: (emoji) => (
<div style={{ padding: "4px 8px" }}>:{emoji}:</div>
),
},
],
}),
]}
/>
);
}With API Data Source
import { MDXEditor } from "@mdxeditor/editor";
import { typeaheadPlugin } from "@mdxeditor/typeahead-plugin";
function App() {
return (
<MDXEditor
markdown="# Hello World"
plugins={[
typeaheadPlugin({
configs: [
{
type: "user",
trigger: "@",
searchCallback: async (query) => {
const response = await fetch(
`/api/users/search?q=${encodeURIComponent(query)}`,
);
const users = await response.json();
return users.map((u) => u.username);
},
renderMenuItem: (username) => (
<div className="user-menu-item">
<span className="username">@{username}</span>
</div>
),
maxResults: 10,
},
],
}),
]}
/>
);
}API
Plugin Configuration
The plugin accepts a single parameter object:
interface TypeaheadPluginParams {
configs: TypeaheadConfig[];
}TypeaheadConfig
Each typeahead configuration has the following properties:
| Property | Type | Required | Description |
| --------------------------- | ---------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------- |
| type | string | Yes | Unique identifier for this typeahead type. Used as MDAST directive name (e.g., :mention[...]) |
| trigger | string | Yes | Trigger character(s) - e.g., "@", "#", ":" |
| searchCallback | (query: string) => Promise<string[]> | Yes | Async function to resolve search results |
| renderMenuItem | (item: string) => JSX.Element | Yes | Render function for menu items |
| Editor | React.ComponentType<TypeaheadEditorProps> | No | Custom component for rendering typeahead nodes in the editor after insertion |
| convertToId | (item: T) => string | No | Converter function to extract string ID from complex items |
| maxResults | number | No | Max results to show (default: 5) |
| nodeClassName | string | No | Custom CSS class for the typeahead node in the editor |
| menuClassName | string | No | Custom CSS class for the autocomplete menu container |
| menuItemClassName | string | No | Custom CSS class for menu items |
| menuItemSelectedClassName | string | No | Custom CSS class for the selected menu item |
| renderMenu | (props: MenuRenderProps) => JSX.Element | No | Custom menu container renderer |
| renderMenuItemWrapper | (props: MenuItemWrapperProps) => JSX.Element | No | Custom menu item wrapper renderer |
| loadingIndicator | JSX.Element | No | Custom loading indicator component to replace default "Loading..." text |
How It Works
When you type a trigger character (e.g., @), the plugin:
- Detects the trigger and activates the typeahead
- Calls your
searchCallbackwith the user's query - Displays matching results in an autocomplete menu
- Inserts the selected item as a custom node in the editor
- Exports to markdown as a text directive:
:mention[Alice],:hashtag[important], etc.
Markdown Persistence
The plugin uses text directives syntax for markdown persistence:
- Input: Type
@Aliceand select from menu - In Lexical: Stored as
TypeaheadNodewithtype: "mention"andcontent: "Alice" - In Markdown: Exported as
:mention[Alice]
Styling
The plugin is unstyled by default, giving you complete control over appearance. Choose the approach that fits your project:
1. Default Styles (Optional)
Import the included default styles for a quick start:
import "@mdxeditor/typeahead-plugin/styles.css";2. Custom CSS Classes
Add custom CSS classes to style different parts:
typeaheadPlugin({
configs: [
{
type: "mention",
trigger: "@",
// Style the inserted node in the editor
nodeClassName: "my-mention-node",
// Style the autocomplete menu
menuClassName: "my-menu",
// Style menu items
menuItemClassName: "my-menu-item",
// Style selected menu item
menuItemSelectedClassName: "my-menu-item-selected",
searchCallback: async (query) => fetchUsers(query),
renderMenuItem: (user) => <div>{user}</div>,
},
],
});Tailwind example:
nodeClassName: "bg-blue-100 text-blue-800 px-2 py-1 rounded-full font-semibold";
menuClassName: "bg-white border rounded-lg shadow-lg p-2";
menuItemClassName: "px-3 py-2 rounded hover:bg-gray-100";
menuItemSelectedClassName: "bg-blue-50 text-blue-700";3. Data Attributes
Style using semantic data attributes:
/* Style all typeahead nodes */
[data-typeahead="true"] {
padding: 2px 6px;
border-radius: 4px;
}
/* Style specific types */
[data-typeahead-type="mention"] {
background: rgba(59, 130, 246, 0.1);
color: #2563eb;
}
/* Style the menu */
[data-typeahead-menu="true"] {
background: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Style menu items */
[data-typeahead-item="true"]:hover {
background: #f3f4f6;
}
[data-typeahead-item="true"][data-selected="true"] {
background: #dbeafe;
}4. Custom Render Functions
For complete control with component libraries like shadcn/ui:
import { Command, CommandItem } from "@/components/ui/command";
import { Popover, PopoverContent } from "@/components/ui/popover";
typeaheadPlugin({
configs: [
{
type: "mention",
trigger: "@",
searchCallback: async (query) => fetchUsers(query),
renderMenuItem: (user) => <UserAvatar user={user} />,
// Custom menu renderer
renderMenu: ({ children, className }) => (
<Popover>
<PopoverContent className={cn("w-[300px] p-0", className)}>
<Command>{children}</Command>
</PopoverContent>
</Popover>
),
// Custom item wrapper
renderMenuItemWrapper: ({
children,
isSelected,
onClick,
onMouseEnter,
}) => (
<CommandItem
className={isSelected ? "bg-accent" : ""}
onClick={onClick}
onMouseEnter={onMouseEnter}
>
{children}
</CommandItem>
),
},
],
});5. Custom Editor Rendering
Customize how typeahead nodes appear in the editor after insertion using the Editor prop:
import { useCellValue } from "@mdxeditor/gurx";
typeaheadPlugin({
configs: [
{
type: "mention",
trigger: "@",
searchCallback: async (query) => fetchUsers(query),
renderMenuItem: (user) => <span>@{user}</span>,
// Custom editor renderer
Editor: ({ node, descriptor }) => {
const content = node.getContent();
return (
<span className="mention-chip">
<Avatar user={content} />
{descriptor.trigger}
{content}
</span>
);
},
},
],
});The Editor component receives:
node- The TypeaheadNode instance with methods likegetContent(),getTrigger(),getTypeaheadType()descriptor- The full descriptor config for this typeahead type
Example with rich user data:
interface User {
id: string;
username: string;
avatarUrl: string;
}
typeaheadPlugin({
configs: [
{
type: "mention",
trigger: "@",
searchCallback: async (query) => {
const response = await fetch(`/api/users?q=${query}`);
return await response.json();
},
renderMenuItem: (user: User) => (
<div className="flex items-center gap-2">
<img src={user.avatarUrl} className="w-6 h-6 rounded-full" />
<span>@{user.username}</span>
</div>
),
convertToId: (user: User) => user.username,
// Render mentions with avatars in the editor
Editor: ({ node }) => {
const username = node.getContent();
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
useEffect(() => {
fetch(`/api/users/${username}`)
.then((res) => res.json())
.then((user) => setAvatarUrl(user.avatarUrl));
}, [username]);
return (
<span className="inline-flex items-center gap-1 bg-blue-50 text-blue-700 px-2 py-0.5 rounded-full">
{avatarUrl && (
<img src={avatarUrl} className="w-4 h-4 rounded-full" />
)}
@{username}
</span>
);
},
},
],
});If Editor is not provided, a default renderer displays trigger + content with appropriate CSS classes.
Peer Dependencies
This plugin requires:
react^18.0.0 || ^19.0.0react-dom^18.0.0 || ^19.0.0
Contributing
This package is part of the MDXEditor Extras monorepo. See the main repository for contribution guidelines.
License
MIT
