Skip to main content
Extensions are the modular building blocks of Tiptap. Every feature in Tiptap is packaged as an extension, whether it’s a node (like paragraphs or headings), a mark (like bold or italic), or functionality (like history or placeholder).

What are Extensions?

Tiptap has three types of extensions:

Extensions

Generic extensions that add functionality without defining content structure (e.g., StarterKit, Placeholder, CharacterCount)

Nodes

Block or inline content types that make up your document (e.g., Paragraph, Heading, Image)

Marks

Formatting that can be applied to text (e.g., Bold, Italic, Link)
All three types share the same underlying Extendable base class and can be used interchangeably in the extensions array.

Using Extensions

Extensions are passed to the editor via the extensions option:
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { Bold } from '@tiptap/extension-bold'
import { Italic } from '@tiptap/extension-italic'

const editor = new Editor({
  extensions: [
    StarterKit,
    Bold,
    Italic,
  ],
})

Configuring Extensions

Most extensions accept options that can be configured using the configure() method:
import { Editor } from '@tiptap/core'
import { Heading } from '@tiptap/extension-heading'
import { Link } from '@tiptap/extension-link'

const editor = new Editor({
  extensions: [
    Heading.configure({
      levels: [1, 2, 3], // Only allow h1, h2, h3
      HTMLAttributes: {
        class: 'my-heading',
      },
    }),
    Link.configure({
      openOnClick: false,
      HTMLAttributes: {
        class: 'my-link',
        rel: 'noopener noreferrer',
      },
    }),
  ],
})

Extension Structure

Every extension is created using the Extension.create(), Node.create(), or Mark.create() static methods:
import { Extension } from '@tiptap/core'

export const MyExtension = Extension.create({
  name: 'myExtension',
  
  addOptions() {
    return {
      // Default options
      myOption: 'default value',
    }
  },
  
  addCommands() {
    return {
      myCommand: () => ({ commands }) => {
        // Command implementation
        return true
      },
    }
  },
  
  addKeyboardShortcuts() {
    return {
      'Mod-k': () => this.editor.commands.myCommand(),
    }
  },
  
  // More hooks...
})
Source: /home/daytona/workspace/source/packages/core/src/Extension.ts:23

Extension API

Configuration

name
string
required
The unique name of the extension. This is used to identify the extension.
name: 'myExtension'
priority
number
default:"100"
The priority determines the order in which extensions are loaded. Higher priority extensions are loaded first.
priority: 1000
addOptions
() => Options
Define default options for your extension.
addOptions() {
  return {
    HTMLAttributes: {},
    openOnClick: true,
  }
}
addStorage
() => Storage
Define storage that persists across the lifetime of the editor.
addStorage() {
  return {
    count: 0,
    users: [],
  }
}
Access storage via editor.storage.extensionName:
editor.storage.myExtension.count

Commands

addCommands
() => Commands
Add commands that can be called via editor.commands.
addCommands() {
  return {
    setAwesome: () => ({ commands }) => {
      return commands.insertContent('<p>Awesome!</p>')
    },
  }
}
Usage:
editor.commands.setAwesome()

Keyboard Shortcuts

addKeyboardShortcuts
() => Record<string, () => boolean>
Add keyboard shortcuts for your extension.
addKeyboardShortcuts() {
  return {
    'Mod-b': () => this.editor.commands.toggleBold(),
    'Mod-Shift-x': () => this.editor.commands.myCommand(),
  }
}
Mod is Cmd on Mac and Ctrl on Windows/Linux.

Input Rules

addInputRules
() => InputRule[]
Add input rules for markdown-style shortcuts.
addInputRules() {
  return [
    markInputRule({
      find: /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*)$/,
      type: this.type,
    }),
  ]
}
This would convert **text** to bold text as you type.

Paste Rules

addPasteRules
() => PasteRule[]
Add paste rules for handling pasted content.
addPasteRules() {
  return [
    markPasteRule({
      find: /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*)/g,
      type: this.type,
    }),
  ]
}

ProseMirror Plugins

addProseMirrorPlugins
() => Plugin[]
Add ProseMirror plugins to extend functionality.
addProseMirrorPlugins() {
  return [
    new Plugin({
      key: new PluginKey('myPlugin'),
      // Plugin configuration
    }),
  ]
}

Global Attributes

addGlobalAttributes
() => GlobalAttribute[]
Add attributes to multiple node or mark types.
addGlobalAttributes() {
  return [
    {
      types: ['heading', 'paragraph'],
      attributes: {
        textAlign: {
          default: 'left',
          renderHTML: attributes => ({
            style: `text-align: ${attributes.textAlign}`,
          }),
          parseHTML: element => element.style.textAlign || 'left',
        },
      },
    },
  ]
}

Lifecycle Hooks

onCreate
() => void
Called when the editor is created.
onCreate() {
  console.log('Extension initialized')
}
onUpdate
() => void
Called when the editor content changes.
onUpdate() {
  this.storage.count++
}
onSelectionUpdate
() => void
Called when the selection changes.
onTransaction
({ transaction }) => void
Called for every transaction.
onFocus
({ event }) => void
Called when the editor receives focus.
onBlur
({ event }) => void
Called when the editor loses focus.
onDestroy
() => void
Called when the editor is destroyed. Use this to clean up resources.
onDestroy() {
  // Clean up event listeners, timers, etc.
}

Creating an Extension

Here’s a complete example of a custom extension:
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'

export interface CharacterCountOptions {
  limit: number | null
}

export const CharacterCount = Extension.create<CharacterCountOptions>({
  name: 'characterCount',
  
  addOptions() {
    return {
      limit: null,
    }
  },
  
  addStorage() {
    return {
      characters: () => {
        return this.editor.state.doc.textContent.length
      },
      words: () => {
        return this.editor.state.doc.textContent.split(/\s+/).filter(Boolean).length
      },
    }
  },
  
  onCreate() {
    console.log(`Character limit: ${this.options.limit}`)
  },
  
  onUpdate() {
    const count = this.storage.characters()
    
    if (this.options.limit && count > this.options.limit) {
      console.warn('Character limit exceeded!')
    }
  },
  
  addProseMirrorPlugins() {
    return [
      new Plugin({
        key: new PluginKey('characterCount'),
        // Plugin logic
      }),
    ]
  },
})

Usage

const editor = new Editor({
  extensions: [
    CharacterCount.configure({
      limit: 1000,
    }),
  ],
})

// Access storage
const count = editor.storage.characterCount.characters()
const words = editor.storage.characterCount.words()

Extending Extensions

You can extend existing extensions to modify or add functionality:
import { Paragraph } from '@tiptap/extension-paragraph'

const CustomParagraph = Paragraph.extend({
  addAttributes() {
    return {
      ...this.parent?.(),
      textAlign: {
        default: 'left',
        renderHTML: attributes => ({
          style: `text-align: ${attributes.textAlign}`,
        }),
        parseHTML: element => element.style.textAlign || 'left',
      },
    }
  },
})
The this.parent?.() call merges the parent extension’s attributes with your new ones.

Extension Packages

Tiptap provides many official extensions:

Starter Kit

import StarterKit from '@tiptap/starter-kit'

const editor = new Editor({
  extensions: [
    StarterKit,
  ],
})
The StarterKit includes:
  • Document
  • Paragraph
  • Text
  • Bold
  • Italic
  • Strike
  • Code
  • Heading
  • Blockquote
  • BulletList
  • OrderedList
  • ListItem
  • CodeBlock
  • HardBreak
  • HorizontalRule
  • History
  • Dropcursor
  • Gapcursor

Individual Extensions

import { Bold } from '@tiptap/extension-bold'
import { Italic } from '@tiptap/extension-italic'
import { Underline } from '@tiptap/extension-underline'
import { Link } from '@tiptap/extension-link'
import { Image } from '@tiptap/extension-image'
import { Table } from '@tiptap/extension-table'
import { TableRow } from '@tiptap/extension-table-row'
import { TableCell } from '@tiptap/extension-table-cell'
import { Placeholder } from '@tiptap/extension-placeholder'
import { CharacterCount } from '@tiptap/extension-character-count'

Real-World Example: Bold Extension

Here’s the actual implementation of the Bold extension from the Tiptap source:
import { Mark, markInputRule, markPasteRule, mergeAttributes } from '@tiptap/core'

export interface BoldOptions {
  HTMLAttributes: Record<string, any>
}

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    bold: {
      setBold: () => ReturnType
      toggleBold: () => ReturnType
      unsetBold: () => ReturnType
    }
  }
}

const starInputRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))$/
const starPasteRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))/g

export const Bold = Mark.create<BoldOptions>({
  name: 'bold',
  
  addOptions() {
    return {
      HTMLAttributes: {},
    }
  },
  
  parseHTML() {
    return [
      { tag: 'strong' },
      { tag: 'b', getAttrs: node => (node as HTMLElement).style.fontWeight !== 'normal' && null },
      { style: 'font-weight', getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null },
    ]
  },
  
  renderHTML({ HTMLAttributes }) {
    return ['strong', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
  },
  
  addCommands() {
    return {
      setBold: () => ({ commands }) => commands.setMark(this.name),
      toggleBold: () => ({ commands }) => commands.toggleMark(this.name),
      unsetBold: () => ({ commands }) => commands.unsetMark(this.name),
    }
  },
  
  addKeyboardShortcuts() {
    return {
      'Mod-b': () => this.editor.commands.toggleBold(),
      'Mod-B': () => this.editor.commands.toggleBold(),
    }
  },
  
  addInputRules() {
    return [
      markInputRule({ find: starInputRegex, type: this.type }),
    ]
  },
  
  addPasteRules() {
    return [
      markPasteRule({ find: starPasteRegex, type: this.type }),
    ]
  },
})
Source: /home/daytona/workspace/source/packages/extension-bold/src/bold.tsx:56

TypeScript Support

import { Extension } from '@tiptap/core'
import type { Editor } from '@tiptap/core'

interface MyOptions {
  color: string
  size: number
}

interface MyStorage {
  count: number
}

export const MyExtension = Extension.create<MyOptions, MyStorage>({
  name: 'myExtension',
  
  addOptions() {
    return {
      color: 'blue',
      size: 12,
    }
  },
  
  addStorage() {
    return {
      count: 0,
    }
  },
})

Best Practices

Unique Names

Always use unique extension names to avoid conflicts.
name: 'myCompany_myExtension'

Clean Up Resources

Use onDestroy to clean up event listeners, timers, and other resources.
onDestroy() {
  this.observer?.disconnect()
  clearInterval(this.timer)
}

Use TypeScript

Define types for your options and storage for better developer experience.

Test Thoroughly

Extensions can interact in unexpected ways. Test your extension with various combinations of other extensions.

Nodes & Marks

Learn about creating node and mark extensions

Commands

Learn about the command system

Schema

Learn about how extensions generate the schema

Editor

Learn about the Editor class