Skip to main content

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.
Title
string
Text
Description
string
Just start typing with plain text.
Search terms
string[]
p, paragraph
Icon
component
<Text size={18} />
editor.chain()
  .focus()
  .deleteRange(range)
  .toggleNode('paragraph', 'paragraph')
  .run()

To-do List

Create interactive checkboxes for task tracking.
Title
string
To-do List
Description
string
Track tasks with a to-do list.
Search terms
string[]
todo, task, list, check, checkbox
Icon
component
<CheckSquare size={18} />
editor.chain()
  .focus()
  .deleteRange(range)
  .toggleTaskList()
  .run()

Heading 1

Large section heading.
Title
string
Heading 1
Description
string
Big section heading.
Search terms
string[]
title, big, large
Icon
component
<Heading1 size={18} />
editor.chain()
  .focus()
  .deleteRange(range)
  .setNode('heading', { level: 1 })
  .run()

Heading 2

Medium section heading.
Title
string
Heading 2
Description
string
Medium section heading.
Search terms
string[]
subtitle, medium
Icon
component
<Heading2 size={18} />
editor.chain()
  .focus()
  .deleteRange(range)
  .setNode('heading', { level: 2 })
  .run()

Heading 3

Small section heading.
Title
string
Heading 3
Description
string
Small section heading.
Search terms
string[]
subtitle, small
Icon
component
<Heading3 size={18} />
editor.chain()
  .focus()
  .deleteRange(range)
  .setNode('heading', { level: 3 })
  .run()

Bullet List

Create unordered list with bullets.
Title
string
Bullet List
Description
string
Create a simple bullet list.
Search terms
string[]
unordered, point
Icon
component
<List size={18} />
editor.chain()
  .focus()
  .deleteRange(range)
  .toggleBulletList()
  .run()

Numbered List

Create ordered list with numbers.
Title
string
Numbered List
Description
string
Create a list with numbering.
Search terms
string[]
ordered
Icon
component
<ListOrdered size={18} />
editor.chain()
  .focus()
  .deleteRange(range)
  .toggleOrderedList()
  .run()

Quote

Insert blockquote for citations.
Title
string
Quote
Description
string
Capture a quote.
Search terms
string[]
blockquote
Icon
component
<TextQuote size={18} />
editor.chain()
  .focus()
  .deleteRange(range)
  .toggleNode('paragraph', 'paragraph')
  .toggleBlockquote()
  .run()

Code

Insert code block with syntax highlighting.
Title
string
Code
Description
string
Capture a code snippet.
Search terms
string[]
codeblock
Icon
component
<Code size={18} />
editor.chain()
  .focus()
  .deleteRange(range)
  .toggleCodeBlock()
  .run()

Image

Upload image from computer.
Title
string
Image
Description
string
Upload an image from your computer.
Search terms
string[]
photo, picture, media
Icon
component
<ImageIcon size={18} />
{
  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.
Title
string
Youtube
Description
string
Embed a Youtube video.
Search terms
string[]
video, youtube, embed
Icon
component
<Youtube size={18} />
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()
}

Twitter

Embed tweet.
Title
string
Twitter
Description
string
Embed a Tweet.
Search terms
string[]
twitter, embed
Icon
component
<Twitter size={18} />
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()
}

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.
Add multiple search terms to make commands easy to find:
searchTerms: ['todo', 'task', 'list', 'check', 'checkbox']
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

Build docs developers (and LLMs) love