How to Customize the Slash Command Menu with New Commands in reactjs-tiptap-editor
You can customize the slash command menu by providing a custom CommandList array to the <SlashCommandList /> component, which replaces or extends the default groups stored in the reactive signal at src/store/commandList.ts.
The Slash Command extension in hunghg255/reactjs-tiptap-editor implements a suggestion-based popup that appears when users type "/". By understanding how the command list flows from the signal store to the UI components, you can inject bespoke formatting options, custom embeds, or entirely new command groups tailored to your application’s workflow.
Understanding the Slash Command Architecture
The extension follows a reactive pattern where the command list lives in a global signal and gets filtered at runtime based on the user’s query.
The Command List Structure
The type definitions in src/extensions/SlashCommand/types.ts establish the contract for all slash menu items. A CommandList is an array of groups, where each group contains a name, title, and an array of Command objects. Each command requires:
name: Unique identifierlabel: User-facing textaliases: Optional array for alternative search termsiconName: Icon identifier for the UIaction: Callback receiving the editor instance and matched range- Optional visibility logic via
shouldBeHiddenandisActivecallbacks
The Reactive Signal Store
Instead of prop-drilling, the extension uses src/store/commandList.ts to hold the current list in a reactive signal accessed via useSignalCommandList. The SlashCommandList component (src/extensions/SlashCommand/components/SlashCommandList.tsx) acts as the entry point: if you omit the commandList prop, it calls renderCommandListDefault({ t }) from src/extensions/SlashCommand/renderCommandListDefault.ts and pushes the result into the signal. If you provide a custom list, it stores yours instead, giving you full control over the menu contents.
Creating Custom Slash Commands
To add new commands, define an array matching the CommandList interface with your custom groups and actions.
First, create a new file (e.g., src/custom/mySlashCommands.ts) and import the necessary types:
import { CommandList } from '@/extensions/SlashCommand/types';
import { t } from '@/locales';
export const mySlashCommands: CommandList[] = [
{
name: 'format',
title: t('editor.slash.format'),
commands: [
{
name: 'highlight',
label: 'Highlight',
aliases: ['hl'],
iconName: 'Highlighter',
shouldBeHidden: (editor) => editor.isActive('codeBlock'),
isActive: (editor) => editor.isActive('highlight'),
action: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
editor.chain().toggleHighlight().run();
},
},
],
},
{
name: 'insert',
title: t('editor.slash.insert'),
commands: [
{
name: 'blockquote',
label: t('editor.blockquote.tooltip'),
iconName: 'TextQuote',
action: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).setBlockquote().run();
},
},
],
},
];
Key implementation details:
- Delete the range first: Always call
deleteRange(range)in your action to remove the typed "/" character before executing the command. - Conditional visibility: Use
shouldBeHiddento hide commands in incompatible contexts (e.g., disabling a highlight command inside code blocks). - Active state: Implement
isActiveto provide visual feedback when the cursor is inside content where the command has already been applied.
Integrating Custom Commands into Your Editor
Pass your custom array to the SlashCommandList component inside your editor UI. Ensure you import SlashCommand from the slashcommand sub-package to keep the suggestion plugin active.
import { RichTextProvider, useEditor, EditorContent } from 'reactjs-tiptap-editor';
import { SlashCommand, SlashCommandList } from 'reactjs-tiptap-editor/slashcommand';
import { mySlashCommands } from '@/custom/mySlashCommands';
export const App = () => {
const editor = useEditor({
extensions: [
// ...other extensions
SlashCommand,
],
});
return (
<RichTextProvider editor={editor}>
<SlashCommandList commandList={mySlashCommands} />
<EditorContent editor={editor} />
</RichTextProvider>
);
};
When the component mounts, it updates the signal in src/store/commandList.ts with your custom data. The SlashCommandNodeView component (src/extensions/SlashCommand/components/SlashCommandNodeView.tsx) then reads this signal to render the floating menu, while useFilterCommandList handles query filtering as the user types.
Replacing vs. Extending the Default List
Complete replacement: If you want to hide all default commands (headings, lists, embeds, etc.), simply provide a full CommandList array covering all your desired functionality without importing renderCommandListDefault.
Extension: To keep the defaults and add your own, import the default renderer, spread its result, and append your custom groups:
import { renderCommandListDefault } from '@/extensions/SlashCommand/renderCommandListDefault';
const customList = [
...renderCommandListDefault({ t }),
{
name: 'custom',
title: 'Custom Tools',
commands: [ /* your commands */ ],
},
];
Summary
- The slash command menu is driven by a reactive signal in
src/store/commandList.tsthat stores an array ofCommandListgroups. - You customize the menu by passing a
commandListprop to<SlashCommandList />, which overrides the default call torenderCommandListDefault. - Each command object requires an
actioncallback that receives the TipTap editor instance and the matched text range. - Use
shouldBeHiddenandisActivecallbacks to control command visibility and highlights based on editor state. - The filtering logic in
renderCommandListDefault.tsautomatically handles search queries against command names, labels, and aliases.
Frequently Asked Questions
How do I hide specific default commands without rewriting the entire list?
You cannot selectively hide individual default commands through props. Instead, import renderCommandListDefault, then filter or map over the returned array to remove unwanted items before passing the modified array to SlashCommandList.
Can I add async actions or API calls inside a slash command?
Yes. The action callback receives the editor instance and can execute any asynchronous logic. Simply ensure you handle the range deletion synchronously first, then perform your async operation and subsequent editor updates.
Why is my custom command still showing inside a code block?
Verify that your shouldBeHidden callback correctly checks the editor state. The function receives the editor instance; return true to hide the command. For example: shouldBeHidden: (editor) => editor.isActive('codeBlock') prevents the command from appearing inside code blocks.
Where is the slash command suggestion plugin actually registered?
The suggestion logic is registered in src/extensions/SlashCommand/SlashCommand.ts, which configures the TipTap suggestion plugin to trigger on the "/" character and coordinates with the SlashCommandNodeView component to position the popup.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →