Suggestion utility

VersionDownloads

This utility helps with all kinds of suggestions in the editor. Have a look at the Mention or Emoji node to see it in action.

Settings

char

The character that triggers the autocomplete popup.

Default: '@'

pluginKey

A ProseMirror PluginKey.

Default: SuggestionPluginKey

allow

A function that returns a boolean to indicate if the suggestion should be active.

Default: (props: { editor: Editor; state: EditorState; range: Range, isActive?: boolean }) => true

shouldShow

A function that returns a boolean to indicate if the suggestion should be active. This is useful to prevent suggestions from opening for remote users in collaborative environments.

shouldShow: ({ editor, range, query, text, transaction }) => {
  return !isChangeOrigin(transaction)
}

Default: null

shouldResetDismissed

Controls when a dismissed suggestion becomes active again after the user closes it with Escape or exitSuggestion().

By default, the utility keeps the dismissed suggestion closed while the user stays in the same trigger context. With allowSpaces: true, that dismissed context can span spaces until the cursor leaves it.

Use shouldResetDismissed if you need to decide yourself when that dismissed context should be cleared.

The callback is not called for every transaction. It is only evaluated on transactions where the suggestion plugin finds a valid match, the match passes allow and shouldShow, and there is a dismissed suggestion state to potentially clear.

Suggestion({
  // ...
  shouldResetDismissed: ({ transaction, allowSpaces, range, match }) => {
    if (!allowSpaces) {
      return false
    }

    return transaction.doc.textBetween(range.from, match.range.to, '\n').includes('.')
  },
})

Return true to clear the dismissed state for the current transaction and allow the suggestion to open again.

Default: null

allowSpaces

Allows or disallows spaces in suggested items.

Default: false

allowedPrefixes

The prefix characters that are allowed to trigger a suggestion. Set to null to allow any prefix character.

Default: [' ']

startOfLine

Trigger the autocomplete popup at the start of a line only.

Default: false

minQueryLength

The minimum number of characters the user must type after the trigger character before the items() function is called.

Useful when fetching suggestions from an API — it prevents unnecessary network requests when the query is too short to be meaningful.

Default: 0

Suggestion({
  minQueryLength: 2,
  items: async ({ query, signal }) => {
    // Only called when query.length >= 2
    const response = await fetch(`/api/search?q=${query}`, { signal })
    return response.json()
  },
})

debounce

The number of milliseconds to wait after the user stops typing before calling items(). This prevents expensive operations (like API requests) from being triggered on every keystroke.

The debounce timer resets on each keystroke, so the items() function is only called once the user pauses typing for the specified duration.

Default: 0 (no debounce)

Suggestion({
  debounce: 300,
  items: async ({ query }) => {
    // Called 300ms after the user stops typing
    return searchApi(query)
  },
})

initialItems

An array of items shown immediately when the suggestion popup opens, before the async items() call resolves. After items() completes, its result replaces the initial items.

Useful for showing recent or popular items while the real results are loading.

Suggestion({
  initialItems: ['Lea Thompson', 'Cyndi Lauper', 'Tom Cruise'],
  items: async ({ query }) => {
    // These replace the initial items once resolved
    return fetchSearchResults(query)
  },
})

Default: undefined

decorationTag

The HTML tag that should be rendered for the suggestion.

Default: 'span'

decorationClass

A CSS class that should be added to the suggestion.

Default: 'suggestion'

decorationContent

The content that should be rendered in the suggestion decoration.

Default: ''

decorationEmptyClass

A CSS class that should be added to the suggestion when it is empty.

Default: 'is-empty'

placement

The preferred placement of the suggestion popup relative to the cursor or trigger decoration. Uses Floating UI placement values.

The resolved placement is forwarded to SuggestionProps.floatingUi.placement, which you can read in your render callbacks.

Default: 'bottom-start'

Suggestion({
  placement: 'top-start',
})

offset

Offsets the suggestion popup from its anchor by the specified number of pixels on each axis. These values are forwarded to the Floating UI offset middleware.

Default: {}

Suggestion({
  offset: { mainAxis: 8, crossAxis: 4 },
})

flip

When enabled, the popup automatically flips to the opposite side when there isn't enough space in the preferred direction. This toggles the Floating UI flip middleware.

Default: true

Suggestion({
  placement: 'top-start',
  flip: false,
})

container

A CSS selector string or an HTMLElement that defines the container for the suggestion popup. Setting this can help with scroll containment or z-index stacking contexts.

Default: undefined

Suggestion({
  container: '#my-editor-container',
})

floatingUi

Advanced Floating UI configuration for cases where the default options don't offer enough control. When provided, these options take precedence over the top-level placement, offset, and flip settings.

import { flip, shift, size } from '@floating-ui/dom'

Suggestion({
  floatingUi: {
    placement: 'top-start',
    strategy: 'fixed',
    middleware: [
      flip({ padding: 8 }),
      shift({ padding: 8 }),
    ],
  },
})

All properties are optional. The full type is:

type SuggestionFloatingUiOptions = {
  placement?: 'top' | 'top-start' | 'top-end'
    | 'bottom' | 'bottom-start' | 'bottom-end'
    | 'right' | 'right-start' | 'right-end'
    | 'left' | 'left-start' | 'left-end'
  strategy?: 'absolute' | 'fixed'
  middleware?: Middleware[]
}

Default: undefined

command

Executed when a suggestion is selected.

Default: () => {}

items

A function or async function that returns filtered suggestions based on user input. The function receives an AbortSignal (signal) which is aborted when the user continues typing (e.g., a new keystroke makes the current request stale). Use this to cancel in-progress fetch requests.

items: async ({ editor, query, signal }) => {
  const response = await fetch(`/api/search?q=${query}`, { signal })
  return response.json()
}

Default: ({ editor, query, signal }) => []

render

A render function for the autocomplete popup. Returns an object with lifecycle hooks (onStart, onBeforeStart, onUpdate, onBeforeUpdate, onExit, onKeyDown) that receive a SuggestionProps object.

The SuggestionProps object passed to each hook includes the following additional properties when items() is async:

{
  // ... existing props (editor, range, query, text, items, command, decorationNode, clientRect) ...

  /** True while an async items() call is in progress, false otherwise */
  loading: boolean

  /** Resolved Floating UI configuration passed through from the options */
  floatingUi: {
    placement: SuggestionPlacement
    strategy: 'absolute' | 'fixed'
    middleware: Middleware[]
    offset: { mainAxis?: number; crossAxis?: number }
  }
}

Default: () => ({})

findSuggestionMatch

Optional param to replace the built-in regex matching of editor content that triggers a suggestion. See the source for more detail.

Default: findSuggestionMatch(config: Trigger): SuggestionMatch

Exiting open suggestions

Sometimes you want your users to be able to exit an an open suggestion without selecting an item. To achieve this, users can either press Escape which will close the open suggestion. If you want to manually trigger the closing of the suggestion, you can use use exitSuggestion utility function to close existing suggestions on your view.

import { exitSuggestion } from '@tiptap/suggestion'
import { PluginKey } from 'prosemirror-state' // optional, if you need to create a custom key

const MySuggestionPluginKey = new PluginKey('my-suggestions') // or use the default 'suggestion'

exitSuggestion(editor.view, MySuggestionPluginKey)

// Alternatively, use the default plugin key:
// exitSuggestion(editor.view, 'suggestion')

Collaboration

When using Tiptap in a collaborative environment, you might notice that suggestions (like mentions) pop open for all users when one user triggers them. This happens because the document change is synced to all clients, and each client's suggestion plugin reacts to the change.

To prevent this, use the shouldShow option combined with the isChangeOrigin helper from @tiptap/extension-collaboration.

import { isChangeOrigin } from '@tiptap/extension-collaboration'

Suggestion({
  // …
  shouldShow: ({ transaction }) => {
    return !isChangeOrigin(transaction)
  },
})

By returning !isChangeOrigin(props.transaction), the suggestion will only be activated if the current user initiated the change.

Source code

packages/suggestion/