Find out what's new in Tiptap Editor 3.0

Suggestion Menu

A powerful and flexible suggestion menu system for Tiptap editors. Creates floating dropdown menus triggered by configurable characters with full keyboard navigation, filtering, and customizable rendering support.

Installation

Add the component via the Tiptap CLI:

npx @tiptap/cli@latest add suggestion-menu

Components

<SuggestionMenu />

The core suggestion menu component that provides a floating dropdown interface triggered by typing specific characters.

Usage

<SuggestionMenu
  editor={editor}
  char="@"
  pluginKey="myMentionMenu"
  items={async ({ query, editor }) => [
    {
      title: 'John Doe',
      subtext: 'Software Engineer',
      onSelect: ({ editor, range }) => {
        editor.chain().focus().insertContentAt(range, '@john').run()
      },
    },
  ]}
>
  {({ items, selectedIndex, onSelect }) => (
    <MenuList items={items} selectedIndex={selectedIndex} onSelect={onSelect} />
  )}
</SuggestionMenu>

Props

NameTypeDefaultDescription
editorEditor | nullundefinedThe Tiptap editor instance
charstring"@"Character that triggers the suggestion menu
items(props) => Item[] | Promise<Item[]>() => []Function returning suggestion items
children(props) => ReactNodeundefinedRender function for menu content
floatingOptionsPartial<UseFloatingOptions>undefinedAdditional floating UI positioning options
selectorstring"tiptap-suggestion-menu"CSS selector for the menu container
pluginKeystring | PluginKeySuggestionPluginKeyUnique identifier for the suggestion plugin
allowSpacesbooleanfalseAllow spaces in suggestion queries
allowToIncludeCharbooleanfalseInclude trigger character in query
allowedPrefixesstring[] | null[" "]Characters that can precede the trigger
startOfLinebooleanfalseOnly trigger at line start
decorationTagstring"span"HTML tag for decoration element
decorationClassstring"suggestion"CSS class for decoration styling
decorationContentstring""Placeholder text in decoration

Types

SuggestionItem<T>

Interface defining the structure of suggestion items.

interface SuggestionItem<T = any> {
  title: string // Main display text
  subtext?: string // Secondary context text
  badge?: IconComponent | string // Icon or badge component
  group?: string // Group identifier for organization
  keywords?: string[] // Additional search keywords
  context?: T // Custom data passed to onSelect
  onSelect: (props: {
    // Selection handler
    editor: Editor
    range: Range
    context?: T
  }) => void
}

SuggestionMenuRenderProps<T>

Props passed to the children render function.

type SuggestionMenuRenderProps<T = any> = {
  items: SuggestionItem<T>[] // Filtered suggestion items
  selectedIndex?: number // Currently selected item index
  onSelect: (item: SuggestionItem<T>) => void // Item selection handler
}

Utilities

filterSuggestionItems(items, query)

Filters and prioritizes suggestion items based on a search query.

import { filterSuggestionItems } from '@/registry/tiptap-ui-utils/suggestion-menu'

const filteredItems = filterSuggestionItems(allItems, 'john')

Parameters

NameTypeDescription
itemsSuggestionItem[]Array of suggestion items to filter
querystringSearch query string

Filtering Logic

  • Matches against title, subtext, and keywords properties
  • Case-insensitive matching
  • Prioritizes exact matches and "starts with" matches
  • Returns empty array for empty queries

calculateStartPosition(cursorPosition, previousNode, triggerChar)

Calculates the start position of a suggestion command in the text.

import { calculateStartPosition } from '@/registry/tiptap-ui-utils/suggestion-menu'

const startPos = calculateStartPosition(100, textNode, '@')

Parameters

NameTypeDescription
cursorPositionnumberCurrent cursor position in document
previousNodeNode | nullText node before cursor
triggerCharstringCharacter that triggered suggestions

Advanced Usage

Custom Suggestion Menu

Create a custom suggestion menu with full control over items and rendering:

function CustomMentionMenu() {
  const getSuggestionItems = async ({ query, editor }) => {
    const users = await fetchUsers(query)

    return users.map((user) => ({
      title: user.name,
      subtext: user.email,
      context: user,
      badge: UserIcon,
      keywords: [user.department, user.role],
      onSelect: ({ editor, range, context }) => {
        editor
          .chain()
          .focus()
          .insertContentAt(range, {
            type: 'mention',
            attrs: { id: context.id, label: context.name },
          })
          .run()
      },
    }))
  }

  return (
    <SuggestionMenu
      char="@"
      pluginKey="customMention"
      items={getSuggestionItems}
      allowSpaces={false}
      decorationClass="my-mention-decoration"
    >
      {({ items, selectedIndex, onSelect }) => (
        <Card>
          <CardBody>
            {items.map((item, index) => (
              <MentionItem
                key={item.title}
                item={item}
                isSelected={index === selectedIndex}
                onSelect={() => onSelect(item)}
              />
            ))}
          </CardBody>
        </Card>
      )}
    </SuggestionMenu>
  )
}

With Grouped Items

Organize suggestions into groups for better navigation:

function GroupedSuggestionMenu() {
  const getSuggestionItems = async ({ query, editor }) => {
    return [
      {
        title: 'Add Heading',
        group: 'Formatting',
        badge: HeadingIcon,
        onSelect: ({ editor, range }) => {
          editor.chain().focus().deleteRange(range).toggleHeading({ level: 1 }).run()
        },
      },
      {
        title: 'Add Table',
        group: 'Insert',
        badge: TableIcon,
        onSelect: ({ editor, range }) => {
          editor.chain().focus().deleteRange(range).insertTable().run()
        },
      },
    ]
  }

  return (
    <SuggestionMenu char="/" items={getSuggestionItems}>
      {({ items, selectedIndex, onSelect }) => (
        <Card>
          <CardBody>
            {Object.entries(groupBy(items, 'group')).map(([groupName, groupItems]) => (
              <div key={groupName}>
                <CardGroupLabel>{groupName}</CardGroupLabel>
                <CardItemGroup>
                  {groupItems.map((item, index) => (
                    <SuggestionItem
                      key={item.title}
                      item={item}
                      isSelected={index === selectedIndex}
                      onSelect={() => onSelect(item)}
                    />
                  ))}
                </CardItemGroup>
              </div>
            ))}
          </CardBody>
        </Card>
      )}
    </SuggestionMenu>
  )
}