Find out what's new in Tiptap V3

Extension API

The power of Tiptap lies in its flexibility. You can create your own extensions from scratch and build a unique editor experience tailored to your needs.

The base extension structure is the same for all extensions, whether you're creating a node, a mark, or a functionality change. And, everything in Tiptap is based on extensions.

Creating an extension

Extensions add new capabilities to Tiptap. You'll read the word "extension" a lot in the docs, even for nodes and marks. But there are literal extensions, too. These can't add to the schema (like marks and nodes do), but they can add functionality or change the behavior of the editor.

A good example would be something that listens to the editor's events and does something with them. Like this:

import { Extension } from '@tiptap/core'

const CustomExtension = Extension.create({
  name: 'customExtension',

  onUpdate() {
    console.log(this.editor.getJSON())
  },
})

You can also use a callback function to create an extension. This is useful if you want to encapsulate the logic of your extension, for example when you want to define event handlers or other custom logic.

import { Extension } from '@tiptap/core'

const CustomExtension = Extension.create(() => {
  // Define variables or functions to use inside your extension
  const customVariable = 'foo'

  function onCreate() {}
  function onUpdate() {}

  return {
    name: 'customExtension',
    onCreate,
    onUpdate,

    // Your code goes here.
  }
})

This extension listens to the editor's update event and logs the editor's current JSON representation to the console.

It is installed to the editor like this:

import { Editor } from '@tiptap/core'

const editor = new Editor({
  extensions: [CustomExtension],
})

// Or if using React or Vue

const editor = useEditor({
  extensions: [CustomExtension],
})

This extensions array can contain any number of extensions. They will be installed in the order they are listed, or sorted by their priority property.

Now that we've seen the basic structure of an extension, let's dive into all of the extension options you can use to create your own extensions.

Extension options

When creating an extension, you can define a set of options that can be configured by the user. These options can be used to customize the behavior of the extension, or to provide additional functionality.

name

The name of the extension. This is used to identify the extension in the editor's extension manager.

const CustomExtension = Extension.create({
  name: 'customExtension',
})

If the extension is a node or a mark, the name is used to identify the node or mark in the editor's schema and therefore persisted to the JSON representation of the editor's content. See store your content as JSON for more information.

priority

The priority defines the order in which extensions are registered. The default priority is 100, that’s what most extension have. Extensions with a higher priority will be loaded earlier.

import Link from '@tiptap/extension-link'

const CustomLink = Link.extend({
  priority: 1000,
})

The order in which extensions are loaded influences two things:

  1. Plugin order (ProseMirror plugins of extensions with a higher priority will run first.)
  2. Schema order

The Link mark for example has a higher priority, which means it will be rendered as <a href="…"><strong>Example</strong></a> instead of <strong><a href="…">Example</a></strong>.

addOptions

The addOptions method is used to define the extension's options. This method should return an object with the options that can be configured by the user.

type CustomExtensionOptions = {
  customOption: string
}

declare module '@tiptap/core' {
  interface ExtensionOptions {
    customOption: CustomExtensionOptions
  }
}

const CustomExtension = Extension.create<CustomExtensionOptions>({
  name: 'customExtension',

  addOptions() {
    return {
      customOption: 'default value',
    }
  },
})

This exposes configuration which can be set when installing the extension:

const editor = new Editor({
  extensions: [CustomExtension.configure({ customOption: 'new value' })],
})

addStorage

The addStorage method is used to define the extension's storage (essentially a simple state manager). This method should return an object with the storage that can be used by the extension.

type CustomExtensionStorage = {
  customValue: string
}

declare module '@tiptap/core' {
  interface ExtensionStorage {
    customExtension: CustomExtensionStorage
  }
}

const CustomExtension = Extension.create<any, CustomExtensionStorage>({
  name: 'customExtension',

  addStorage() {
    return {
      customValue: 'default value',
    }
  },
})

This exposes storage which can be accessed within the extension:

const CustomExtension = Extension.create<any, CustomExtensionStorage>({
  name: 'customExtension',

  addStorage() {
    return {
      customValue: 'default value',
    }
  },

  onUpdate() {
    console.log(this.storage.customValue) // 'default value'
  },
})

Or, by the editor:

const editor = new Editor({
  extensions: [CustomExtension],
})

editor.storage.customExtension.customValue // 'default value'

Notice

The editor.storage is namespaced by the extension's name.

addCommands

The addCommands method is used to define the extension's commands. This method should return an object with the commands that can be executed by the user.

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    customExtension: {
      customCommand: () => ReturnType
    }
  }
}

const CustomExtension = Extension.create({
  name: 'customExtension',

  addCommands() {
    return {
      customCommand:
        () =>
        ({ commands }) => {
          return commands.setContent('Custom command executed')
        },
    }
  },
})

Use the commands parameter inside of addCommands

To access other commands inside addCommands use the commands parameter that’s passed to it.

This exposes commands which can be executed by the user:

const editor = new Editor({
  extensions: [CustomExtension],
})

editor.commands.customCommand() // 'Custom command executed'
editor.chain().customCommand().run() // 'Custom command executed'

addKeyboardShortcuts

The addKeyboardShortcuts method is used to define keyboard shortcuts. This method should return an object with the keyboard shortcuts that can be used by the user.

const CustomExtension = Extension.create({
  name: 'customExtension',

  addKeyboardShortcuts() {
    return {
      'Mod-k': () => {
        console.log('Keyboard shortcut executed')
      },
    }
  },
})

This exposes keyboard shortcuts which can be used by the user.

addInputRules

With input rules you can define regular expressions to listen for user inputs. They are used for markdown shortcuts, or for example to convert text like (c) to a © (and many more) with the Typography extension. Use the markInputRule helper function for marks, and the nodeInputRule for nodes.

By default text between two tildes on both sides is transformed to striked text. If you want to think one tilde on each side is enough, you can overwrite the input rule like this:

import { Extension } from '@tiptap/core'
import { markInputRule } from '@tiptap/core'

const CustomExtension = Extension.create({
  name: 'customExtension',

  addInputRules() {
    return [
      markInputRule({
        find: /(?:~)((?:[^~]+))(?:~)$/,
        type: this.editor.schema.marks.strike,
      }),
    ]
  },
})

Now, when you type ~striked text~, it will be transformed to striked text.

addPasteRules

Paste rules work like input rules (see above) do. But instead of listening to what the user types, they are applied to pasted content.

There is one tiny difference in the regular expression. Input rules typically end with a $ dollar sign (which means “asserts position at the end of a line”), paste rules typically look through all the content and don’t have said $ dollar sign.

Taking the example from above and applying it to the paste rule would look like the following example.

import { Extension } from '@tiptap/core'
import { markPasteRule } from '@tiptap/core'

const CustomExtension = Extension.create({
  name: 'customExtension',

  addPasteRules() {
    return [
      markPasteRule({
        find: /(?:~)((?:[^~]+))(?:~)/g,
        type: this.editor.schema.marks.strike,
      }),
    ]
  },
})

Events

You can even move your event listeners to a separate extension. Here is an example with listeners for all events:

import { Extension } from '@tiptap/core'

const CustomExtension = Extension.create({
  onBeforeCreate() {
    // The editor is about to be created.
  },
  onCreate() {
    // The editor is ready.
  },
  onUpdate() {
    // The content has changed.
  },
  onSelectionUpdate({ editor }) {
    // The selection has changed.
  },
  onTransaction({ transaction }) {
    // The editor state has changed.
  },
  onFocus({ event }) {
    // The editor is focused.
  },
  onBlur({ event }) {
    // The editor isn’t focused anymore.
  },
  onDestroy() {
    // The editor is being destroyed.
  },
})

addProseMirrorPlugins

You can add ProseMirror plugins to your extension. This is useful if you want to extend the editor with ProseMirror plugins.

Using an existing ProseMirror plugin

You can wrap existing ProseMirror plugins in Tiptap extensions like shown in the example below.

import { history } from '@tiptap/pm/history'

const History = Extension.create({
  addProseMirrorPlugins() {
    return [
      history(),
      // …
    ]
  },
})

Creating a ProseMirror plugin

You can also create custom ProseMirror plugins. Here is an example of a custom ProseMirror plugin that logs a message to the console.

import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Extension } from '@tiptap/core'

const CustomExtension = Extension.create({
  name: 'customExtension',

  addProseMirrorPlugins() {
    return [
      new Plugin({
        key: new PluginKey('customPlugin'),
        view() {
          return {
            update() {
              console.log('Custom plugin updated')
            },
          }
        },
      }),
    ]
  },
})

To learn more about ProseMirror plugins, check out the ProseMirror documentation.

addExtensions

You can add more extensions to your extension. This is useful if you want to create a bundle of extensions that belong together.

import { Extension } from '@tiptap/core'
import CustomExtension1 from './CustomExtension1'

const CustomExtension = Extension.create({
  name: 'customExtension',

  addExtensions() {
    return [
      CustomExtension1.configure({
        name: 'customExtension1',
      }),
    ]
  },
})

extendNodeSchema

You can extend the editor's NodeConfig with the extendNodeSchema method. This is useful if you want to add additional attributes to the node schema.

import { Extension } from '@tiptap/core'

declare module '@tiptap/core' {
  // This adds a new configuration option to the NodeConfig
  interface NodeConfig {
    customAttribute: {
      default: null
    }
  }
}

const CustomExtension = Extension.create({
  name: 'customExtension',

  extendNodeSchema() {
    return {
      customAttribute: {
        default: null,
      },
    }
  },
})

extendMarkSchema

You can extend the editor's MarkConfig with the extendMarkSchema method. This is useful if you want to add additional attributes to the mark schema.

import { Extension } from '@tiptap/core'

declare module '@tiptap/core' {
  // This adds a new configuration option to the MarkConfig
  interface MarkConfig {
    customAttribute: {
      default: null
    }
  }
}

const CustomExtension = Extension.create({
  name: 'customExtension',

  extendMarkSchema() {
    return {
      customAttribute: {
        default: null,
      },
    }
  },
})

What’s available in this?

Those extensions aren’t classes, but you still have a few important things available in this everywhere in the extension.

// Name of the extension, for example 'bulletList'
this.name

// Editor instance
this.editor

// ProseMirror type (if a node or mark)
this.type

// Object with all settings
this.options

// Everything that’s in the extended extension
this.parent

// Storage object
this.storage