Skip to main content

Custom Extensions

Extensions are the building blocks of Tiptap. They allow you to add new functionality to the editor without modifying nodes or marks. Extensions are perfect for adding editor-wide features like commands, keyboard shortcuts, or plugins.

Understanding Extensions

The Extension class is the base class for all extensions in Tiptap. It provides a rich API for extending the editorโ€™s functionality.
import { Extension } from '@tiptap/core'

export const MyExtension = Extension.create({
  name: 'myExtension',
  
  addOptions() {
    return {
      // Default options
    }
  },
  
  addCommands() {
    return {
      // Custom commands
    }
  },
  
  addKeyboardShortcuts() {
    return {
      // Keyboard shortcuts
    }
  },
  
  addProseMirrorPlugins() {
    return [
      // ProseMirror plugins
    ]
  },
})

Creating a Simple Extension

Letโ€™s create a simple extension that replaces emoticons with emojis as you type.
import { Extension, textInputRule } from '@tiptap/core'

export const SmilieReplacer = Extension.create({
  name: 'smilieReplacer',

  addInputRules() {
    return [
      textInputRule({ find: /:-\) $/, replace: '๐Ÿ™‚ ' }),
      textInputRule({ find: /:D $/, replace: '๐Ÿ˜ƒ ' }),
      textInputRule({ find: /;-\) $/, replace: '๐Ÿ˜‰ ' }),
      textInputRule({ find: /:-\( $/, replace: '๐Ÿ˜ž ' }),
      textInputRule({ find: /<3 $/, replace: 'โค๏ธ ' }),
      textInputRule({ find: /\/shrug $/, replace: 'ยฏ\\_(ใƒ„)_/ยฏ' }),
    ]
  },
})
Source: demos/src/Examples/Savvy/React/SmilieReplacer.ts:1

Extension with ProseMirror Plugins

Hereโ€™s a more advanced extension that uses ProseMirror plugins to highlight color codes in the editor.
import { Extension } from '@tiptap/core'
import { Plugin } from '@tiptap/pm/state'
import findColors from './findColors'

export const ColorHighlighter = Extension.create({
  name: 'colorHighlighter',

  addProseMirrorPlugins() {
    return [
      new Plugin({
        state: {
          init(_, { doc }) {
            return findColors(doc)
          },
          apply(transaction, oldState) {
            return transaction.docChanged 
              ? findColors(transaction.doc) 
              : oldState
          },
        },
        props: {
          decorations(state) {
            return this.getState(state)
          },
        },
      }),
    ]
  },
})
Source: demos/src/Examples/Savvy/React/ColorHighlighter.ts:1

Extension Configuration

Adding Options

Extensions can accept configuration options that customize their behavior.
import { Extension } from '@tiptap/core'

interface MyExtensionOptions {
  prefix: string
  maxLength: number
}

export const MyExtension = Extension.create<MyExtensionOptions>({
  name: 'myExtension',
  
  addOptions() {
    return {
      prefix: 'default',
      maxLength: 100,
    }
  },
  
  addCommands() {
    return {
      insertPrefix: () => ({ commands }) => {
        return commands.insertContent(this.options.prefix)
      },
    }
  },
})

Configuring Extensions

import { Editor } from '@tiptap/core'
import { MyExtension } from './my-extension'

const editor = new Editor({
  extensions: [
    MyExtension.configure({
      prefix: 'custom',
      maxLength: 200,
    }),
  ],
})

Extension Storage

Extensions can maintain their own state using storage.
import { Extension } from '@tiptap/core'

interface MyStorage {
  count: number
  items: string[]
}

export const MyExtension = Extension.create<{}, MyStorage>({
  name: 'myExtension',
  
  addStorage() {
    return {
      count: 0,
      items: [],
    }
  },
  
  onCreate() {
    this.storage.count = 0
  },
  
  addCommands() {
    return {
      incrementCount: () => ({ editor }) => {
        this.storage.count++
        return true
      },
      addItem: (item: string) => ({ editor }) => {
        this.storage.items.push(item)
        return true
      },
    }
  },
})

Lifecycle Hooks

Extensions can hook into various editor lifecycle events.
import { Extension } from '@tiptap/core'

export const MyExtension = Extension.create({
  name: 'myExtension',
  
  onBeforeCreate() {
    // Before the editor is created
    console.log('Before create')
  },
  
  onCreate() {
    // Editor is ready
    console.log('Editor created')
  },
  
  onUpdate() {
    // Content has changed
    console.log('Content updated')
  },
  
  onSelectionUpdate() {
    // Selection has changed
    console.log('Selection changed')
  },
  
  onTransaction({ transaction }) {
    // On every transaction
    console.log('Transaction:', transaction)
  },
  
  onFocus() {
    // Editor gained focus
    console.log('Editor focused')
  },
  
  onBlur() {
    // Editor lost focus
    console.log('Editor blurred')
  },
  
  onDestroy() {
    // Editor is being destroyed
    console.log('Editor destroyed')
  },
})
Source: packages/core/src/Extendable.ts:341

Keyboard Shortcuts

Add custom keyboard shortcuts to your extension.
import { Extension } from '@tiptap/core'

export const MyExtension = Extension.create({
  name: 'myExtension',
  
  addKeyboardShortcuts() {
    return {
      // Mod is Cmd on Mac, Ctrl on Windows/Linux
      'Mod-k': () => {
        console.log('Mod-k pressed')
        return true
      },
      
      // Multiple modifiers
      'Mod-Shift-z': () => {
        this.editor.commands.redo()
        return true
      },
      
      // Handle Enter key
      'Enter': () => {
        return this.editor.commands.first([
          () => this.editor.commands.newlineInCode(),
          () => this.editor.commands.createParagraphNear(),
        ])
      },
    }
  },
})
Source: packages/core/src/Extendable.ts:133

Global Attributes

Extensions can add attributes to other nodes or marks.
import { Extension } from '@tiptap/core'

export const TextAlign = Extension.create({
  name: 'textAlign',
  
  addOptions() {
    return {
      types: ['heading', 'paragraph'],
      alignments: ['left', 'center', 'right', 'justify'],
      defaultAlignment: 'left',
    }
  },
  
  addGlobalAttributes() {
    return [
      {
        types: this.options.types,
        attributes: {
          textAlign: {
            default: this.options.defaultAlignment,
            parseHTML: element => element.style.textAlign || this.options.defaultAlignment,
            renderHTML: attributes => {
              if (attributes.textAlign === this.options.defaultAlignment) {
                return {}
              }
              return { style: `text-align: ${attributes.textAlign}` }
            },
          },
        },
      },
    ]
  },
  
  addCommands() {
    return {
      setTextAlign: (alignment: string) => ({ commands }) => {
        if (!this.options.alignments.includes(alignment)) {
          return false
        }
        return this.options.types.every(type => 
          commands.updateAttributes(type, { textAlign: alignment })
        )
      },
    }
  },
})
Source: packages/core/src/Extendable.ts:79

Priority

Control the order in which extensions are loaded and executed.
import { Extension } from '@tiptap/core'

export const MyExtension = Extension.create({
  name: 'myExtension',
  priority: 1000, // Higher priority loads earlier (default: 100)
  
  // This extension will take precedence over lower-priority extensions
})
Source: packages/core/src/Extendable.ts:50

Extending Existing Extensions

You can extend existing extensions to customize their behavior.
import { Extension } from '@tiptap/core'
import { SmilieReplacer } from './smilie-replacer'

export const CustomSmilieReplacer = SmilieReplacer.extend({
  addInputRules() {
    return [
      // Keep parent rules
      ...this.parent?.() || [],
      // Add new rules
      textInputRule({ find: /:wave: $/, replace: '๐Ÿ‘‹ ' }),
      textInputRule({ find: /:fire: $/, replace: '๐Ÿ”ฅ ' }),
    ]
  },
})
Source: packages/core/src/Extension.ts:35
Extensions are ideal for adding editor-wide functionality. Use Nodes for block content and Marks for inline formatting.

Next Steps

Custom Nodes

Learn how to create custom block content

Custom Marks

Create custom inline formatting

Build docs developers (and LLMs) love