npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@mdxeditor/typeahead-plugin

v0.4.0

Published

MDXEditor plugin for typeahead functionality

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-plugin

Features

  • 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:

  1. Detects the trigger and activates the typeahead
  2. Calls your searchCallback with the user's query
  3. Displays matching results in an autocomplete menu
  4. Inserts the selected item as a custom node in the editor
  5. 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 @Alice and select from menu
  • In Lexical: Stored as TypeaheadNode with type: "mention" and content: "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 like getContent(), 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.0
  • react-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