Suggestion plugin for Tiptap that provides triggered autocomplete functionality for mentions, hashtags, and other contextual suggestions
npx @tessl/cli install tessl/npm-tiptap--suggestion@3.4.0Tiptap Suggestion is a utility plugin for Tiptap editors that enables triggered autocomplete functionality such as mentions, hashtags, slash commands, and other contextual suggestions. It provides a flexible ProseMirror plugin that detects trigger characters and offers lifecycle hooks for rendering custom suggestion interfaces.
npm install @tiptap/suggestionimport { Suggestion, exitSuggestion, findSuggestionMatch, SuggestionPluginKey } from "@tiptap/suggestion";For default import:
import Suggestion from "@tiptap/suggestion";For CommonJS:
const { Suggestion, exitSuggestion, findSuggestionMatch, SuggestionPluginKey } = require("@tiptap/suggestion");import { Editor } from "@tiptap/core";
import { Suggestion } from "@tiptap/suggestion";
const editor = new Editor({
// ... other config
});
// Create a suggestion plugin for mentions
const mentionSuggestion = Suggestion({
editor: editor,
char: '@',
items: ({ query }) => {
return [
{ id: 1, label: 'John Doe' },
{ id: 2, label: 'Jane Smith' },
{ id: 3, label: 'Bob Wilson' }
].filter(item =>
item.label.toLowerCase().includes(query.toLowerCase())
);
},
render: () => ({
onStart: (props) => {
// Create and show suggestion dropdown
console.log('Suggestion started', props.query);
},
onUpdate: (props) => {
// Update suggestion dropdown with new items
console.log('Suggestion updated', props.items);
},
onExit: () => {
// Hide suggestion dropdown
console.log('Suggestion exited');
},
onKeyDown: ({ event }) => {
// Handle keyboard navigation
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
return true; // Handled
}
return false; // Not handled
}
}),
command: ({ editor, range, props }) => {
// Insert the selected mention
editor.chain().focus().insertContentAt(range, `@${props.label}`).run();
},
});
// Add the plugin to your editor
editor.registerPlugin(mentionSuggestion);Creates a ProseMirror plugin that handles suggestion functionality with customizable behavior and rendering.
/**
* Creates a suggestion plugin for Tiptap editors
* @param options - Configuration options for the suggestion behavior
* @returns ProseMirror Plugin instance
*/
function Suggestion<I = any, TSelected = any>(
options: SuggestionOptions<I, TSelected>
): Plugin<any>;
interface SuggestionOptions<I = any, TSelected = any> {
/** The plugin key for the suggestion plugin (default: SuggestionPluginKey) */
pluginKey?: PluginKey;
/** The editor instance (required) */
editor: Editor;
/** The character that triggers the suggestion (default: '@') */
char?: string;
/** Allow spaces in the suggestion query (default: false) */
allowSpaces?: boolean;
/** Allow the trigger character to be included in the query (default: false) */
allowToIncludeChar?: boolean;
/** Allowed prefix characters before trigger (default: [' ']) */
allowedPrefixes?: string[] | null;
/** Only match suggestions at the start of the line (default: false) */
startOfLine?: boolean;
/** HTML tag name for decoration node (default: 'span') */
decorationTag?: string;
/** CSS class for decoration node (default: 'suggestion') */
decorationClass?: string;
/** Content for decoration node (default: '') */
decorationContent?: string;
/** CSS class when decoration is empty (default: 'is-empty') */
decorationEmptyClass?: string;
/** Function called when suggestion is selected */
command?: (props: { editor: Editor; range: Range; props: TSelected }) => void;
/** Function returning suggestion items array */
items?: (props: { query: string; editor: Editor }) => I[] | Promise<I[]>;
/** Function returning render lifecycle hooks */
render?: () => {
/** Called before suggestion starts */
onBeforeStart?: (props: SuggestionProps<I, TSelected>) => void;
/** Called when suggestion starts */
onStart?: (props: SuggestionProps<I, TSelected>) => void;
/** Called before suggestion updates */
onBeforeUpdate?: (props: SuggestionProps<I, TSelected>) => void;
/** Called when suggestion updates */
onUpdate?: (props: SuggestionProps<I, TSelected>) => void;
/** Called when suggestion exits */
onExit?: (props: SuggestionProps<I, TSelected>) => void;
/** Called on keydown events, return true if handled */
onKeyDown?: (props: SuggestionKeyDownProps) => boolean;
};
/** Function determining if suggestion should be active */
allow?: (props: { editor: Editor; state: EditorState; range: Range; isActive?: boolean }) => boolean;
/** Custom match finding function */
findSuggestionMatch?: typeof findSuggestionMatch;
}Finds suggestion matches in text based on configurable trigger patterns.
/**
* Finds suggestion matches in text based on trigger configuration
* @param config - Trigger configuration object
* @returns SuggestionMatch object or null if no match found
*/
function findSuggestionMatch(config: Trigger): SuggestionMatch;
interface Trigger {
/** Trigger character */
char: string;
/** Whether to allow spaces in queries */
allowSpaces: boolean;
/** Whether to include trigger character in queries */
allowToIncludeChar: boolean;
/** Array of allowed prefix characters */
allowedPrefixes: string[] | null;
/** Whether to only match at line start */
startOfLine: boolean;
/** ProseMirror resolved position */
$position: ResolvedPos;
}
type SuggestionMatch = {
/** Range object with from/to positions */
range: Range;
/** Matched query string (excluding trigger character) */
query: string;
/** Full matched text (including trigger character) */
text: string;
} | null;Programmatically exits suggestion mode.
/**
* Programmatically exits the suggestion mode
* @param view - EditorView instance
* @param pluginKeyRef - PluginKey instance (default: SuggestionPluginKey)
*/
function exitSuggestion(view: EditorView, pluginKeyRef?: PluginKey): void;Default PluginKey instance used by the suggestion plugin when no custom pluginKey is provided.
/**
* Default plugin key for the suggestion plugin
* Used when no custom pluginKey is specified in SuggestionOptions
*/
const SuggestionPluginKey: PluginKey<any>;interface SuggestionProps<I = any, TSelected = any> {
/** The editor instance */
editor: Editor;
/** The range of the suggestion text */
range: Range;
/** Current query string (excluding trigger character) */
query: string;
/** Full suggestion text (including trigger character) */
text: string;
/** Array of suggestion items */
items: I[];
/** Function to execute selected suggestion */
command: (props: TSelected) => void;
/** HTML element of the decoration node */
decorationNode: Element | null;
/** Function returning DOMRect for positioning */
clientRect?: (() => DOMRect | null) | null;
}
interface SuggestionKeyDownProps {
/** EditorView instance */
view: EditorView;
/** KeyboardEvent */
event: KeyboardEvent;
/** Current suggestion range */
range: Range;
}// From @tiptap/core
interface Editor { /* Tiptap editor instance */ }
interface Range { from: number; to: number; }
// From @tiptap/pm/state
class Plugin { /* ProseMirror plugin class */ }
class PluginKey { /* ProseMirror plugin key class */ }
interface EditorState { /* ProseMirror editor state */ }
// From @tiptap/pm/view
interface EditorView { /* ProseMirror editor view */ }
// From @tiptap/pm/model
interface ResolvedPos { /* ProseMirror resolved position */ }import { Suggestion } from "@tiptap/suggestion";
const hashtagSuggestion = Suggestion({
editor: myEditor,
char: '#',
items: ({ query }) => {
return ['javascript', 'typescript', 'react', 'vue']
.filter(tag => tag.includes(query.toLowerCase()))
.map(tag => ({ tag }));
},
render: () => ({
onStart: (props) => {
// Show hashtag dropdown
},
onUpdate: (props) => {
// Update hashtag list
},
onExit: () => {
// Hide dropdown
}
}),
command: ({ editor, range, props }) => {
editor.chain().focus().insertContentAt(range, `#${props.tag}`).run();
}
});import { Suggestion } from "@tiptap/suggestion";
const slashCommandSuggestion = Suggestion({
editor: myEditor,
char: '/',
startOfLine: true,
items: ({ query }) => {
return [
{ title: 'Heading 1', command: 'heading', level: 1 },
{ title: 'Heading 2', command: 'heading', level: 2 },
{ title: 'Bullet List', command: 'bulletList' },
].filter(item =>
item.title.toLowerCase().includes(query.toLowerCase())
);
},
command: ({ editor, range, props }) => {
editor.chain().focus().deleteRange(range);
if (props.command === 'heading') {
editor.chain().setHeading({ level: props.level }).run();
} else if (props.command === 'bulletList') {
editor.chain().toggleBulletList().run();
}
}
});import { Suggestion, findSuggestionMatch } from "@tiptap/suggestion";
const customSuggestion = Suggestion({
editor: myEditor,
char: '$',
allowSpaces: true,
allowedPrefixes: [' ', '(', '['],
findSuggestionMatch: (config) => {
// Custom matching logic
return findSuggestionMatch(config);
},
items: ({ query }) => {
// Return variable suggestions
return variables.filter(v => v.name.includes(query));
}
});