Skip to main content
Commands are functions that modify the editor state. They form the primary API for programmatic changes to the document.

What is a Command?

In ProseMirror (and ProseKit), a command is a function with this signature:
import type { Command } from '@prosekit/pm/state'

type Command = (
  state: EditorState,
  dispatch?: (tr: Transaction) => void,
  view?: EditorView,
) => boolean
A command:
  • Receives the current state, optional dispatch function, and optional view
  • Returns true if it can be applied, false otherwise
  • When dispatch is provided, applies changes by dispatching a transaction
  • When dispatch is omitted, only checks if the command is applicable

Command Actions

ProseKit wraps ProseMirror commands in CommandAction objects (packages/core/src/types/extension-command.ts):
export interface CommandAction<Args extends any[] = any[]> {
  /**
   * Execute the current command. Return true if the command was successfully
   * executed, otherwise false.
   */
  (...args: Args): boolean

  /**
   * Check if the current command can be executed. Return true if the command
   * can be executed, otherwise false.
   */
  canExec(...args: Args): boolean
}

Using Command Actions

const editor = createEditor({ extension })

// Execute a command
const success = editor.commands.toggleBold()

if (success) {
  console.log('Text is now bold')
} else {
  console.log('Could not apply bold')
}

// Check if command can be executed
if (editor.commands.toggleBold.canExec()) {
  // Show "Bold" button as active
}
canExec() is useful for UI state - determining if a button should be enabled or if a mark/node is currently active.

Command Creators

Commands are registered via CommandCreator functions:
export type CommandCreator<Args extends any[] = any[]> = (
  ...arg: Args
) => Command
A command creator:
  • Accepts arguments specific to the command
  • Returns a ProseMirror Command function
  • Is converted to a CommandAction by the editor

Example: Toggle Mark Command

From packages/core/src/commands/toggle-mark.ts:
import { toggleMark as baseToggleMark } from '@prosekit/pm/commands'
import type { Attrs, MarkType } from '@prosekit/pm/model'
import type { Command } from '@prosekit/pm/state'
import type { CommandCreator } from '../types/extension-command.ts'

export interface ToggleMarkOptions {
  /** The mark type to toggle */
  type: string | MarkType

  /** The optional attributes to set on the mark */
  attrs?: Attrs | null

  /** Controls whether the mark is removed (true) or added (false) when partially present */
  removeWhenPresent?: boolean

  /** Whether to enter inline atoms */
  enterInlineAtoms?: boolean
}

export function toggleMark({
  type,
  attrs,
  removeWhenPresent = false,
  enterInlineAtoms = true,
}: ToggleMarkOptions): Command {
  return (state, dispatch, view) => {
    return baseToggleMark(getMarkType(state.schema, type), attrs, {
      removeWhenPresent,
      enterInlineAtoms,
    })(state, dispatch, view)
  }
}

toggleMark satisfies CommandCreator

Built-in Commands

ProseKit provides many built-in commands:

Mark Commands

import { toggleMark, addMark, removeMark, unsetMark, expandMark } from '@prosekit/core'

// Toggle a mark on/off
toggleMark({ type: 'bold' })

// Add a mark
addMark({ type: 'link', attrs: { href: 'https://example.com' } })

// Remove a mark
removeMark({ type: 'bold' })

// Remove a mark and its attributes
unsetMark({ type: 'link' })

// Expand mark to cover entire word
expandMark({ type: 'bold' })

Node Commands

import { 
  setBlockType,
  unsetBlockType,
  toggleNode,
  toggleWrap,
  wrap,
  insertNode,
  removeNode,
  setNodeAttrs,
} from '@prosekit/core'

// Change block type
setBlockType({ type: 'heading', attrs: { level: 1 } })

// Reset to default block type
unsetBlockType()

// Toggle a node (like task item)
toggleNode({ type: 'taskItem' })

// Toggle wrapping (like blockquote)
toggleWrap({ type: 'blockquote' })

// Wrap selection
wrap({ type: 'blockquote' })

// Insert a node
insertNode({ type: 'image', attrs: { src: 'image.jpg' } })

// Remove a node
removeNode({ type: 'image' })

// Update node attributes
setNodeAttrs({ type: 'heading', attrs: { level: 2 } })

Text Commands

import { insertText, selectAll, selectBlock } from '@prosekit/core'

// Insert text
insertText({ text: 'Hello world' })

// Select all content
selectAll()

// Select the current block
selectBlock()

Default Block Commands

import { insertDefaultBlock } from '@prosekit/core'

// Insert a default block node (usually paragraph)
insertDefaultBlock()
Many commands check context - they only work in appropriate situations. For example, setBlockType only works when the selection is in a compatible block.

Registering Commands

Commands are registered through the command facet (packages/core/src/facets/command.ts):
import type { CommandCreators } from '../types/extension-command.ts'
import { defineFacet, type Facet } from './facet.ts'
import { rootFacet, type RootPayload } from './root.ts'

type CommandPayload = CommandCreators

export const commandFacet: Facet<CommandPayload, RootPayload> = defineFacet({
  reducer: (inputs) => {
    switch (inputs.length) {
      case 0:
        return { commands: {} }
      case 1:
        return { commands: inputs[0] }
      default:
        return { commands: Object.assign({}, ...inputs) as CommandCreators }
    }
  },
  parent: rootFacet,
  singleton: true,
})

How Commands are Registered

When an editor is created (packages/core/src/editor/editor.ts:107-110):
if (payload.commands) {
  for (const [name, commandCreator] of Object.entries(payload.commands)) {
    this.defineCommand(name, commandCreator)
  }
}
The defineCommand method wraps the creator in a CommandAction:
public defineCommand<Args extends any[] = any[]>(
  name: string,
  commandCreator: CommandCreator<Args>,
): void {
  const action: CommandAction<Args> = (...args: Args) => {
    const command = commandCreator(...args)
    return this.exec(command)
  }

  const canExec = (...args: Args) => {
    const command = commandCreator(...args)
    return this.canExec(command)
  }

  action.canExec = canExec

  this.commands[name] = action as CommandAction
}

Creating Custom Commands

Create custom commands by implementing the CommandCreator pattern:

Simple Command

import type { Command } from '@prosekit/pm/state'

function insertDivider(): Command {
  return (state, dispatch) => {
    const { schema, tr } = state
    const divider = schema.nodes.horizontalRule.create()
    
    if (dispatch) {
      tr.replaceSelectionWith(divider)
      dispatch(tr)
    }
    
    return true
  }
}

Parameterized Command

import type { Command } from '@prosekit/pm/state'

interface InsertHeadingOptions {
  level: 1 | 2 | 3 | 4 | 5 | 6
}

function insertHeading({ level }: InsertHeadingOptions): Command {
  return (state, dispatch) => {
    const { schema, tr, selection } = state
    const heading = schema.nodes.heading.create({ level })
    
    if (dispatch) {
      tr.replaceSelectionWith(heading)
      dispatch(tr)
    }
    
    return true
  }
}

Command with Validation

import type { Command } from '@prosekit/pm/state'
import { canJoin } from '@prosekit/pm/transform'

function joinBackward(): Command {
  return (state, dispatch) => {
    const { tr, selection } = state
    const { $from } = selection
    
    // Check if we can join
    if (!canJoin(tr.doc, $from.pos)) {
      return false
    }
    
    if (dispatch) {
      tr.join($from.pos)
      dispatch(tr)
    }
    
    return true
  }
}
Always return false if the command cannot be applied. This ensures canExec() works correctly and UI states are accurate.

Command Composition

Compose multiple commands using helper functions:

Chain Commands

Try commands in sequence until one succeeds:
import { chainCommands } from '@prosekit/pm/commands'

const exitCode = chainCommands(
  exitCodeBlock,
  exitCodeMark,
)

Filter Commands

Wrap a command with a condition:
function withCondition(condition: (state: EditorState) => boolean, command: Command): Command {
  return (state, dispatch, view) => {
    if (!condition(state)) {
      return false
    }
    return command(state, dispatch, view)
  }
}

const toggleBoldInParagraph = withCondition(
  (state) => {
    const { $from } = state.selection
    return $from.parent.type.name === 'paragraph'
  },
  toggleMark({ type: 'bold' })
)

Executing Commands

There are multiple ways to execute commands:

1. Through Editor Instance

// Using command actions
editor.commands.toggleBold()

// Using exec method
import { toggleMark } from '@prosekit/core'

const command = toggleMark({ type: 'bold' })
editor.exec(command)

2. Through Transaction

import { toggleMark } from '@prosekit/core'

const command = toggleMark({ type: 'bold' })
const success = command(
  editor.state,
  (tr) => editor.view.dispatch(tr),
  editor.view,
)

3. In Event Handlers

import { defineKeymap } from '@prosekit/core'
import { toggleMark } from '@prosekit/core'

const keymap = defineKeymap({
  'Mod-b': toggleMark({ type: 'bold' }),
  'Mod-i': toggleMark({ type: 'italic' }),
})
Mod means Cmd on Mac and Ctrl on Windows/Linux. ProseKit handles the platform differences automatically.

Command Utilities

ProseKit provides utilities for working with commands:

Get Node/Mark Type

import { getNodeType, getMarkType } from '@prosekit/core'

function myCommand(): Command {
  return (state, dispatch) => {
    const paragraphType = getNodeType(state.schema, 'paragraph')
    const boldType = getMarkType(state.schema, 'bold')
    
    // Use types...
    return true
  }
}

Check Active State

import { isNodeActive, isMarkActive } from '@prosekit/core'

function checkFormatting(state: EditorState): void {
  // Check if heading level 1 is active
  if (isNodeActive(state, 'heading', { level: 1 })) {
    console.log('Currently in H1')
  }
  
  // Check if bold is active
  if (isMarkActive(state, 'bold')) {
    console.log('Bold is active')
  }
}

Find Parent Node

import { findParentNode } from '@prosekit/core'

function getCurrentBlock(state: EditorState) {
  const result = findParentNode(
    (node) => node.type.isBlock,
    state.selection,
  )
  
  if (result) {
    console.log('Current block:', result.node.type.name)
  }
}

Best Practices

1. Validate Before Modifying

// ✅ Good
function insertImage(src: string): Command {
  return (state, dispatch) => {
    const imageType = state.schema.nodes.image
    
    // Check if image node exists
    if (!imageType) {
      return false
    }
    
    // Check if image can be inserted at current position
    const { $from } = state.selection
    if (!$from.parent.type.contentMatch.matchType(imageType)) {
      return false
    }
    
    if (dispatch) {
      const node = imageType.create({ src })
      const tr = state.tr.replaceSelectionWith(node)
      dispatch(tr)
    }
    
    return true
  }
}

// ❌ Bad - no validation
function insertImage(src: string): Command {
  return (state, dispatch) => {
    if (dispatch) {
      const node = state.schema.nodes.image.create({ src })
      dispatch(state.tr.replaceSelectionWith(node))
    }
    return true
  }
}

2. Use Existing Commands

// ✅ Good - reuse existing command
import { toggleMark } from '@prosekit/core'

const command = toggleMark({ type: 'bold' })

// ❌ Bad - reimplementing from scratch
function myToggleBold(): Command {
  return (state, dispatch) => {
    // Complex logic that already exists in toggleMark...
  }
}

3. Provide Meaningful Return Values

// ✅ Good
function splitBlock(): Command {
  return (state, dispatch) => {
    const { $from } = state.selection
    
    // Can't split in a code block
    if ($from.parent.type.spec.code) {
      return false
    }
    
    if (dispatch) {
      dispatch(state.tr.split($from.pos))
    }
    
    return true
  }
}

// ❌ Bad - always returns true
function splitBlock(): Command {
  return (state, dispatch) => {
    if (dispatch) {
      dispatch(state.tr.split(state.selection.$from.pos))
    }
    return true // Always true, even when split fails!
  }
}

4. Keep Commands Pure

// ✅ Good - pure function
function insertText(text: string): Command {
  return (state, dispatch) => {
    if (dispatch) {
      dispatch(state.tr.insertText(text))
    }
    return true
  }
}

// ❌ Bad - side effects
let lastText = ''

function insertText(text: string): Command {
  lastText = text // Side effect!
  return (state, dispatch) => {
    console.log('Inserting:', text) // Side effect!
    if (dispatch) {
      dispatch(state.tr.insertText(text))
    }
    return true
  }
}

Common Patterns

Toggle Commands

Toggle between two states:
function toggleTaskItem(): Command {
  return (state, dispatch) => {
    const { $from } = state.selection
    const taskItemType = state.schema.nodes.taskItem
    
    // Check if we're in a task item
    const inTaskItem = $from.parent.type === taskItemType
    
    if (inTaskItem) {
      // Convert to paragraph
      return setBlockType({ type: 'paragraph' })(state, dispatch)
    } else {
      // Convert to task item
      return setBlockType({ type: 'taskItem' })(state, dispatch)
    }
  }
}

Conditional Commands

Execute commands based on context:
function handleEnter(): Command {
  return (state, dispatch, view) => {
    const { $from } = state.selection
    
    // In code block: insert newline
    if ($from.parent.type.spec.code) {
      return insertText('\n')(state, dispatch)
    }
    
    // In list: create new list item
    if ($from.parent.type.name === 'listItem') {
      return splitListItem(state, dispatch)
    }
    
    // Default: split block
    return splitBlock()(state, dispatch)
  }
}

Batch Commands

Perform multiple operations in one transaction:
function insertCodeBlock(code: string): Command {
  return (state, dispatch) => {
    if (dispatch) {
      const { tr, schema } = state
      const codeBlock = schema.nodes.codeBlock.create(
        null,
        schema.text(code)
      )
      
      // Insert code block and set selection
      tr.replaceSelectionWith(codeBlock)
      tr.setSelection(/* ... */)
      
      dispatch(tr)
    }
    
    return true
  }
}

Next Steps

Schema

Learn about document schemas

Extensions

Understand the extension system