Overview
Slash commands provide a quick way to insert and format content. Type / anywhere in the editor to open the command menu.
Implementation
Slash commands are implemented using Novel’s Command extension:
import { Command, renderItems, createSuggestionItems } from 'novel/extensions'
export const slashCommand = Command.configure({
suggestion: {
items: ({ editor }) => {
const isCurrentUserActive =
editor.storage.multipleCarets?.isCurrentUserActive || false
// Only show suggestions if user is active
return isCurrentUserActive ? suggestionItems : []
},
render: renderItems,
},
})
Commands are only shown when the current user is active in collaborative mode.
Available Commands
Text
Convert to plain text paragraph.
Just start typing with plain text.
editor.chain()
.focus()
.deleteRange(range)
.toggleNode('paragraph', 'paragraph')
.run()
To-do List
Create interactive checkboxes for task tracking.
Track tasks with a to-do list.
todo, task, list, check, checkbox
<CheckSquare size={18} />
editor.chain()
.focus()
.deleteRange(range)
.toggleTaskList()
.run()
Heading 1
Large section heading.
editor.chain()
.focus()
.deleteRange(range)
.setNode('heading', { level: 1 })
.run()
Heading 2
Medium section heading.
editor.chain()
.focus()
.deleteRange(range)
.setNode('heading', { level: 2 })
.run()
Heading 3
Small section heading.
editor.chain()
.focus()
.deleteRange(range)
.setNode('heading', { level: 3 })
.run()
Bullet List
Create unordered list with bullets.
Create a simple bullet list.
editor.chain()
.focus()
.deleteRange(range)
.toggleBulletList()
.run()
Numbered List
Create ordered list with numbers.
Create a list with numbering.
<ListOrdered size={18} />
editor.chain()
.focus()
.deleteRange(range)
.toggleOrderedList()
.run()
Quote
Insert blockquote for citations.
editor.chain()
.focus()
.deleteRange(range)
.toggleNode('paragraph', 'paragraph')
.toggleBlockquote()
.run()
Code
Insert code block with syntax highlighting.
editor.chain()
.focus()
.deleteRange(range)
.toggleCodeBlock()
.run()
Image
Upload image from computer.
Upload an image from your computer.
{
title: 'Image',
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run()
// Create file input
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0]
const pos = editor.view.state.selection.from
uploadFn(file, editor.view, pos)
}
}
input.click()
},
}
Youtube
Embed YouTube video.
Implementation
Supported URLs
const videoLink = prompt('Please enter Youtube Video Link')
const ytregex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/
if (ytregex.test(videoLink)) {
editor.chain()
.focus()
.deleteRange(range)
.setYoutubeVideo({ src: videoLink })
.run()
}
https://www.youtube.com/watch?v=VIDEO_ID
https://youtu.be/VIDEO_ID
https://www.youtube.com/embed/VIDEO_ID
https://m.youtube.com/watch?v=VIDEO_ID
Embed tweet.
Implementation
Supported URLs
const tweetLink = prompt('Please enter Twitter Link')
const tweetRegex = /^https?:\/\/(www\.)?x\.com\/([a-zA-Z0-9_]{1,15})(\/status\/(\d+))?(\/\S*)?$/
if (tweetRegex.test(tweetLink)) {
editor.chain()
.focus()
.deleteRange(range)
.setTweet({ src: tweetLink })
.run()
}
https://x.com/username/status/123456789
https://www.x.com/username/status/123456789
- Both twitter.com and x.com domains
Creating Custom Commands
Add your own slash commands to suggestionItems:
import { createSuggestionItems } from 'novel/extensions'
import { MyIcon } from 'lucide-react'
export const suggestionItems = createSuggestionItems([
// ... existing commands
{
title: 'My Custom Command',
description: 'Does something cool',
searchTerms: ['custom', 'special'],
icon: <MyIcon size={18} />,
command: ({ editor, range }) => {
editor.chain()
.focus()
.deleteRange(range)
// Your custom command logic
.run()
},
},
])
Command Menu UI
The command menu is styled with Tailwind:
<EditorCommand className="z-20 h-auto max-h-[330px] overflow-y-auto rounded-md border border-muted bg-background px-1 py-2 shadow-md transition-all">
<EditorCommandEmpty className="px-2 text-muted-foreground">
No results
</EditorCommandEmpty>
<EditorCommandList>
{suggestionItems.map((item) => (
<EditorCommandItem
value={item.title}
onCommand={(val) => item.command?.(val)}
className="flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm hover:bg-accent aria-selected:bg-accent"
key={item.title}
>
<div className="flex h-10 w-10 items-center justify-center rounded-md border border-muted bg-background">
{item.icon}
</div>
<div>
<p className="font-medium">{item.title}</p>
<p className="text-xs text-muted-foreground">{item.description}</p>
</div>
</EditorCommandItem>
))}
</EditorCommandList>
</EditorCommand>
Keyboard Navigation
Navigate the command menu with:
- ↑/↓: Move selection up/down
- Enter: Execute selected command
- Esc: Close menu
Navigation is handled by:
import { handleCommandNavigation } from 'novel/extensions'
editorProps={{
handleDOMEvents: {
keydown: (_view, event) => handleCommandNavigation(event),
},
}}
Search and Filtering
Commands are filtered by:
- Title (case-insensitive)
- Description
- Search terms
Example: Typing /task will show “To-do List” because it includes “task” in searchTerms.
TypeScript Types
type SuggestionItem = {
title: string
description: string
searchTerms: string[]
icon: React.ReactNode
command: (props: {
editor: Editor
range: Range
}) => void
}
Best Practices
Each command should perform a single, clear action. Complex workflows should be broken into multiple commands.
Use descriptive search terms
Add multiple search terms to make commands easy to find:searchTerms: ['todo', 'task', 'list', 'check', 'checkbox']
Provide clear descriptions
Descriptions help users understand what each command does before executing it.
Icons from Lucide React provide visual context and improve UX.
Next Steps
Extensions
Learn about TipTap extensions
Selectors
Explore bubble menu selectors