Skip to main content
Enter rules allow you to perform special actions when the user presses Enter and the text before the cursor matches a specific pattern. They’re useful for creating smart list behaviors, breaking out of blocks, or inserting special content.

Overview

Enter rules are triggered when:
  1. The user presses the Enter key
  2. The text directly before the cursor matches the rule’s regex pattern
  3. The regex pattern must end with $
ProseKit provides two types of enter rules:
  • Custom Enter Rules: Define fully custom behavior
  • Text Block Enter Rules: Replace matched text with a block node

Basic Usage

import { defineEnterRule } from '@prosekit/extensions/enter-rule'

const extension = defineEnterRule({
  regex: /^- $/,
  handler: ({ state, dispatch, utils }) => {
    // Delete the matched text and insert a list item
    if (dispatch) {
      const tr = state.tr
      const { $from } = state.selection
      const start = $from.start()
      tr.delete(start, $from.pos)
      tr.setBlockType(start, start, state.schema.nodes.listItem)
      dispatch(tr)
    }
    return true
  },
})

Text Block Enter Rules

Replace the matched text with a block node when Enter is pressed.
import { defineTextBlockEnterRule } from '@prosekit/extensions/enter-rule'

const extension = defineTextBlockEnterRule({
  regex: /^---$/,
  type: 'horizontalRule',
})
This will convert a line containing --- into a horizontal rule when the user presses Enter.

API Reference

defineEnterRule

regex
RegExp
required
The regular expression to match against the text before the cursor. Must end with $.
handler
EnterRuleHandler
required
Function called when the pattern matches and Enter is pressed.
type EnterRuleHandler = (options: {
  state: EditorState
  dispatch?: (tr: Transaction) => void
  utils: {
    canSplit: boolean
    matchStart: number
    matchEnd: number
  }
}) => boolean
Return true to indicate the rule handled the event, false to let other handlers process it.

defineTextBlockEnterRule

regex
RegExp
required
The regular expression to match against. Must end with $.
type
string | NodeType
required
The node type to insert when the pattern matches.
attrs
Attrs | null | ((match: RegExpMatchArray) => Attrs | null)
Attributes to set on the new node.

Real-World Examples

Exit Empty List Items

import { defineEnterRule } from '@prosekit/extensions/enter-rule'

const exitListExtension = defineEnterRule({
  regex: /^$/,
  handler: ({ state, dispatch, utils }) => {
    const { $from } = state.selection
    
    // Check if we're in an empty list item
    if ($from.parent.type.name !== 'listItem') {
      return false
    }
    
    if ($from.parent.content.size > 0) {
      return false
    }
    
    // Convert empty list item to paragraph
    if (dispatch) {
      const tr = state.tr
      const pos = $from.before($from.depth)
      tr.setBlockType(pos, pos, state.schema.nodes.paragraph)
      dispatch(tr)
    }
    
    return true
  },
})

Insert Horizontal Rule

import { defineTextBlockEnterRule } from '@prosekit/extensions/enter-rule'

const horizontalRuleExtension = defineTextBlockEnterRule({
  regex: /^---$/,
  type: 'horizontalRule',
})
Type --- and press Enter to insert a horizontal rule.

Smart Code Block Exit

import { defineEnterRule } from '@prosekit/extensions/enter-rule'

const exitCodeBlockExtension = defineEnterRule({
  regex: /^```$/,
  handler: ({ state, dispatch }) => {
    const { $from } = state.selection
    
    // Only trigger in code blocks
    if ($from.parent.type.name !== 'codeBlock') {
      return false
    }
    
    if (dispatch) {
      const tr = state.tr
      // Exit the code block and insert a paragraph
      const after = $from.after($from.depth - 1)
      tr.insert(after, state.schema.nodes.paragraph.create())
      tr.setSelection(
        TextSelection.near(tr.doc.resolve(after + 1))
      )
      dispatch(tr)
    }
    
    return true
  },
})

Break Out of Blockquote

import { defineEnterRule } from '@prosekit/extensions/enter-rule'

const exitBlockquoteExtension = defineEnterRule({
  regex: /^$/,
  handler: ({ state, dispatch }) => {
    const { $from } = state.selection
    
    // Check if we're in an empty paragraph inside a blockquote
    if ($from.parent.type.name !== 'paragraph') {
      return false
    }
    
    if ($from.parent.content.size > 0) {
      return false
    }
    
    // Find the blockquote ancestor
    let blockquoteDepth = -1
    for (let d = $from.depth; d > 0; d--) {
      if ($from.node(d).type.name === 'blockquote') {
        blockquoteDepth = d
        break
      }
    }
    
    if (blockquoteDepth === -1) {
      return false
    }
    
    // Exit the blockquote
    if (dispatch) {
      const tr = state.tr
      const after = $from.after(blockquoteDepth)
      tr.insert(after, state.schema.nodes.paragraph.create())
      tr.setSelection(
        TextSelection.near(tr.doc.resolve(after + 1))
      )
      dispatch(tr)
    }
    
    return true
  },
})

Use Cases

Exit Empty Blocks

Let users break out of lists, blockquotes, or other containers by pressing Enter in empty blocks.

Insert Special Nodes

Create shortcuts for inserting horizontal rules, page breaks, or other special nodes.

Smart List Behavior

Implement custom list continuation, indentation, or exit behavior.

Context-Aware Actions

Perform different actions based on the current context (node type, depth, content).

Tips

Enter rules are checked in the order they’re defined. Return true from your handler to prevent subsequent rules from running.
Use the utils.canSplit property to check if the current position can be split before performing actions.
Be careful with empty patterns (/^$/). They will match every Enter press, so make sure your handler checks the context carefully before taking action.