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
The unique name of the extension. This is used to identify the extension.
The priority determines the order in which extensions are loaded. Higher priority extensions are loaded first.
Define default options for your extension. addOptions () {
return {
HTMLAttributes: {},
openOnClick: true ,
}
}
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
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.
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
Add paste rules for handling pasted content. addPasteRules () {
return [
markPasteRule ({
find: / (?: ^| \s )( \*\* (?! \s + \*\* )((?: [ ^ * ] + )) \*\* ) / g ,
type: this . type ,
}),
]
}
ProseMirror Plugins
Add ProseMirror plugins to extend functionality. addProseMirrorPlugins () {
return [
new Plugin ({
key: new PluginKey ( 'myPlugin' ),
// Plugin configuration
}),
]
}
Global Attributes
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
Called when the editor is created. onCreate () {
console . log ( 'Extension initialized' )
}
Called when the editor content changes. onUpdate () {
this . storage . count ++
}
Called when the selection changes.
onTransaction
({ transaction }) => void
Called for every transaction.
Called when the editor receives focus.
Called when the editor loses focus.
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