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