0
# Extension System
1
2
The extension system is the core of @tiptap/core's modularity, allowing you to add functionality through Extensions, Nodes, and Marks. Extensions handle non-content features, Nodes represent document structure, and Marks represent text formatting.
3
4
## Capabilities
5
6
### Extension Base Class
7
8
Extensions add functionality that doesn't directly represent document content, such as commands, keyboard shortcuts, or plugins.
9
10
```typescript { .api }
11
/**
12
* Base class for creating editor extensions
13
*/
14
class Extension<Options = any, Storage = any> {
15
/**
16
* Create a new extension
17
* @param config - Extension configuration
18
* @returns Extension instance
19
*/
20
static create<O = any, S = any>(
21
config?: Partial<ExtensionConfig<O, S>>
22
): Extension<O, S>;
23
24
/**
25
* Configure the extension with new options
26
* @param options - Options to merge with defaults
27
* @returns New extension instance with updated options
28
*/
29
configure(options?: Partial<Options>): Extension<Options, Storage>;
30
31
/**
32
* Extend the extension with additional configuration
33
* @param extendedConfig - Additional configuration to apply
34
* @returns New extended extension instance
35
*/
36
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(
37
extendedConfig?: Partial<ExtensionConfig<ExtendedOptions, ExtendedStorage>>
38
): Extension<ExtendedOptions, ExtendedStorage>;
39
}
40
41
interface ExtensionConfig<Options = any, Storage = any> {
42
/** Unique name for the extension */
43
name: string;
44
45
/** Default options for the extension */
46
defaultOptions?: Options;
47
48
/** Priority for loading order (higher loads later) */
49
priority?: number;
50
51
/** Initialize storage for sharing data between extensions */
52
addStorage?(): Storage;
53
54
/** Add commands to the editor */
55
addCommands?(): Commands;
56
57
/** Add keyboard shortcuts */
58
addKeymap?(): Record<string, any>;
59
60
/** Add input rules for text transformation */
61
addInputRules?(): InputRule[];
62
63
/** Add paste rules for paste transformation */
64
addPasteRules?(): PasteRule[];
65
66
/** Add global attributes to all nodes */
67
addGlobalAttributes?(): GlobalAttributes[];
68
69
/** Add custom node view renderer */
70
addNodeView?(): NodeViewRenderer;
71
72
/** Add ProseMirror plugins */
73
addProseMirrorPlugins?(): Plugin[];
74
75
/** Called when extension is created */
76
onCreate?(this: { options: Options; storage: Storage }): void;
77
78
/** Called when editor content is updated */
79
onUpdate?(this: { options: Options; storage: Storage }): void;
80
81
/** Called before editor is destroyed */
82
onDestroy?(this: { options: Options; storage: Storage }): void;
83
84
/** Called when selection changes */
85
onSelectionUpdate?(this: { options: Options; storage: Storage }): void;
86
87
/** Called on every transaction */
88
onTransaction?(this: { options: Options; storage: Storage }): void;
89
90
/** Called when editor gains focus */
91
onFocus?(this: { options: Options; storage: Storage }): void;
92
93
/** Called when editor loses focus */
94
onBlur?(this: { options: Options; storage: Storage }): void;
95
}
96
```
97
98
**Usage Examples:**
99
100
```typescript
101
import { Extension } from '@tiptap/core';
102
103
// Simple extension with commands
104
const CustomExtension = Extension.create({
105
name: 'customExtension',
106
107
addCommands() {
108
return {
109
customCommand: (text: string) => ({ commands }) => {
110
return commands.insertContent(text);
111
}
112
};
113
},
114
115
addKeymap() {
116
return {
117
'Mod-k': () => this.editor.commands.customCommand('Shortcut pressed!'),
118
};
119
}
120
});
121
122
// Extension with options and storage
123
const CounterExtension = Extension.create<{ step: number }, { count: number }>({
124
name: 'counter',
125
126
defaultOptions: {
127
step: 1,
128
},
129
130
addStorage() {
131
return {
132
count: 0,
133
};
134
},
135
136
addCommands() {
137
return {
138
increment: () => ({ editor }) => {
139
this.storage.count += this.options.step;
140
return true;
141
},
142
getCount: () => () => {
143
return this.storage.count;
144
}
145
};
146
}
147
});
148
149
// Configure extension
150
const customCounter = CounterExtension.configure({ step: 5 });
151
152
// Extend extension
153
const AdvancedCounter = CounterExtension.extend({
154
name: 'advancedCounter',
155
156
addCommands() {
157
return {
158
...this.parent?.(),
159
reset: () => ({ editor }) => {
160
this.storage.count = 0;
161
return true;
162
}
163
};
164
}
165
});
166
```
167
168
### Node Class
169
170
Nodes represent structural elements in the document like paragraphs, headings, lists, and custom block or inline elements.
171
172
```typescript { .api }
173
/**
174
* Base class for creating document nodes
175
*/
176
class Node<Options = any, Storage = any> {
177
/**
178
* Create a new node extension
179
* @param config - Node configuration
180
* @returns Node extension instance
181
*/
182
static create<O = any, S = any>(
183
config?: Partial<NodeConfig<O, S>>
184
): Node<O, S>;
185
186
/**
187
* Configure the node with new options
188
* @param options - Options to merge with defaults
189
* @returns New node instance with updated options
190
*/
191
configure(options?: Partial<Options>): Node<Options, Storage>;
192
193
/**
194
* Extend the node with additional configuration
195
* @param extendedConfig - Additional configuration to apply
196
* @returns New extended node instance
197
*/
198
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(
199
extendedConfig?: Partial<NodeConfig<ExtendedOptions, ExtendedStorage>>
200
): Node<ExtendedOptions, ExtendedStorage>;
201
}
202
203
interface NodeConfig<Options = any, Storage = any> extends ExtensionConfig<Options, Storage> {
204
/** Content expression defining allowed child content */
205
content?: string | ((this: { options: Options }) => string);
206
207
/** Marks that can be applied to this node */
208
marks?: string | ((this: { options: Options }) => string);
209
210
/** Node group (e.g., 'block', 'inline') */
211
group?: string | ((this: { options: Options }) => string);
212
213
/** Whether this is an inline node */
214
inline?: boolean | ((this: { options: Options }) => boolean);
215
216
/** Whether this node is atomic (cannot be directly edited) */
217
atom?: boolean | ((this: { options: Options }) => boolean);
218
219
/** Whether this node can be selected */
220
selectable?: boolean | ((this: { options: Options }) => boolean);
221
222
/** Whether this node can be dragged */
223
draggable?: boolean | ((this: { options: Options }) => boolean);
224
225
/** Defines how to parse HTML into this node */
226
parseHTML?(): HTMLParseRule[];
227
228
/** Defines how to render this node as HTML */
229
renderHTML?(props: { node: ProseMirrorNode; HTMLAttributes: Record<string, any> }): DOMOutputSpec;
230
231
/** Defines how to render this node as text */
232
renderText?(props: { node: ProseMirrorNode }): string;
233
234
/** Add custom node view */
235
addNodeView?(): NodeViewRenderer;
236
237
/** Define node attributes */
238
addAttributes?(): Record<string, Attribute>;
239
}
240
241
interface HTMLParseRule {
242
tag?: string;
243
node?: string;
244
mark?: string;
245
style?: string;
246
priority?: number;
247
consuming?: boolean;
248
context?: string;
249
getAttrs?: (node: HTMLElement) => Record<string, any> | null | false;
250
}
251
252
interface Attribute {
253
default?: any;
254
rendered?: boolean;
255
renderHTML?: (attributes: Record<string, any>) => Record<string, any> | null;
256
parseHTML?: (element: HTMLElement) => any;
257
keepOnSplit?: boolean;
258
isRequired?: boolean;
259
}
260
```
261
262
**Usage Examples:**
263
264
```typescript
265
import { Node } from '@tiptap/core';
266
267
// Simple custom node
268
const CalloutNode = Node.create({
269
name: 'callout',
270
group: 'block',
271
content: 'block+',
272
273
addAttributes() {
274
return {
275
type: {
276
default: 'info',
277
parseHTML: element => element.getAttribute('data-type'),
278
renderHTML: attributes => ({
279
'data-type': attributes.type,
280
}),
281
},
282
};
283
},
284
285
parseHTML() {
286
return [
287
{
288
tag: 'div[data-callout]',
289
getAttrs: node => ({ type: node.getAttribute('data-type') }),
290
},
291
];
292
},
293
294
renderHTML({ node, HTMLAttributes }) {
295
return [
296
'div',
297
{
298
'data-callout': '',
299
'data-type': node.attrs.type,
300
...HTMLAttributes,
301
},
302
0, // Content goes here
303
];
304
},
305
306
addCommands() {
307
return {
308
setCallout: (type: string) => ({ commands }) => {
309
return commands.wrapIn(this.name, { type });
310
},
311
};
312
},
313
});
314
315
// Inline node example
316
const MentionNode = Node.create({
317
name: 'mention',
318
group: 'inline',
319
inline: true,
320
selectable: false,
321
atom: true,
322
323
addAttributes() {
324
return {
325
id: {
326
default: null,
327
parseHTML: element => element.getAttribute('data-id'),
328
renderHTML: attributes => ({
329
'data-id': attributes.id,
330
}),
331
},
332
label: {
333
default: null,
334
parseHTML: element => element.getAttribute('data-label'),
335
renderHTML: attributes => ({
336
'data-label': attributes.label,
337
}),
338
},
339
};
340
},
341
342
parseHTML() {
343
return [
344
{
345
tag: 'span[data-mention]',
346
},
347
];
348
},
349
350
renderHTML({ node, HTMLAttributes }) {
351
return [
352
'span',
353
{
354
'data-mention': '',
355
'data-id': node.attrs.id,
356
'data-label': node.attrs.label,
357
...HTMLAttributes,
358
},
359
`@${node.attrs.label}`,
360
];
361
},
362
363
renderText({ node }) {
364
return `@${node.attrs.label}`;
365
},
366
367
addCommands() {
368
return {
369
insertMention: (options: { id: string; label: string }) => ({ commands }) => {
370
return commands.insertContent({
371
type: this.name,
372
attrs: options,
373
});
374
},
375
};
376
},
377
});
378
```
379
380
### Mark Class
381
382
Marks represent text formatting that can be applied to ranges of text, such as bold, italic, links, or custom formatting.
383
384
```typescript { .api }
385
/**
386
* Base class for creating text marks
387
*/
388
class Mark<Options = any, Storage = any> {
389
/**
390
* Create a new mark extension
391
* @param config - Mark configuration
392
* @returns Mark extension instance
393
*/
394
static create<O = any, S = any>(
395
config?: Partial<MarkConfig<O, S>>
396
): Mark<O, S>;
397
398
/**
399
* Configure the mark with new options
400
* @param options - Options to merge with defaults
401
* @returns New mark instance with updated options
402
*/
403
configure(options?: Partial<Options>): Mark<Options, Storage>;
404
405
/**
406
* Extend the mark with additional configuration
407
* @param extendedConfig - Additional configuration to apply
408
* @returns New extended mark instance
409
*/
410
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(
411
extendedConfig?: Partial<MarkConfig<ExtendedOptions, ExtendedStorage>>
412
): Mark<ExtendedOptions, ExtendedStorage>;
413
414
/**
415
* Handle mark exit behavior (for marks like links)
416
* @param options - Exit options
417
* @returns Whether the exit was handled
418
*/
419
static handleExit(options: {
420
editor: Editor;
421
mark: ProseMirrorMark
422
}): boolean;
423
}
424
425
interface MarkConfig<Options = any, Storage = any> extends ExtensionConfig<Options, Storage> {
426
/** Whether the mark is inclusive (extends to typed text) */
427
inclusive?: boolean | ((this: { options: Options }) => boolean);
428
429
/** Marks that this mark excludes */
430
excludes?: string | ((this: { options: Options }) => string);
431
432
/** Mark group */
433
group?: string | ((this: { options: Options }) => string);
434
435
/** Whether mark can span across different nodes */
436
spanning?: boolean | ((this: { options: Options }) => boolean);
437
438
/** Whether this is a code mark (excludes other formatting) */
439
code?: boolean | ((this: { options: Options }) => boolean);
440
441
/** Defines how to parse HTML into this mark */
442
parseHTML?(): HTMLParseRule[];
443
444
/** Defines how to render this mark as HTML */
445
renderHTML?(props: {
446
mark: ProseMirrorMark;
447
HTMLAttributes: Record<string, any>
448
}): DOMOutputSpec;
449
450
/** Add custom mark view */
451
addMarkView?(): MarkViewRenderer;
452
453
/** Define mark attributes */
454
addAttributes?(): Record<string, Attribute>;
455
456
/** Handle exit behavior when typing at mark boundary */
457
onExit?(): boolean;
458
}
459
```
460
461
**Usage Examples:**
462
463
```typescript
464
import { Mark } from '@tiptap/core';
465
466
// Simple formatting mark
467
const HighlightMark = Mark.create({
468
name: 'highlight',
469
470
addAttributes() {
471
return {
472
color: {
473
default: 'yellow',
474
parseHTML: element => element.getAttribute('data-color'),
475
renderHTML: attributes => ({
476
'data-color': attributes.color,
477
}),
478
},
479
};
480
},
481
482
parseHTML() {
483
return [
484
{
485
tag: 'mark',
486
},
487
{
488
style: 'background-color',
489
getAttrs: value => ({ color: value }),
490
},
491
];
492
},
493
494
renderHTML({ mark, HTMLAttributes }) {
495
return [
496
'mark',
497
{
498
style: `background-color: ${mark.attrs.color}`,
499
...HTMLAttributes,
500
},
501
0,
502
];
503
},
504
505
addCommands() {
506
return {
507
setHighlight: (color: string = 'yellow') => ({ commands }) => {
508
return commands.setMark(this.name, { color });
509
},
510
toggleHighlight: (color: string = 'yellow') => ({ commands }) => {
511
return commands.toggleMark(this.name, { color });
512
},
513
unsetHighlight: () => ({ commands }) => {
514
return commands.unsetMark(this.name);
515
},
516
};
517
},
518
});
519
520
// Link mark with exit handling
521
const LinkMark = Mark.create({
522
name: 'link',
523
inclusive: false,
524
525
addAttributes() {
526
return {
527
href: {
528
default: null,
529
},
530
target: {
531
default: null,
532
},
533
rel: {
534
default: null,
535
},
536
};
537
},
538
539
parseHTML() {
540
return [
541
{
542
tag: 'a[href]',
543
getAttrs: node => ({
544
href: node.getAttribute('href'),
545
target: node.getAttribute('target'),
546
rel: node.getAttribute('rel'),
547
}),
548
},
549
];
550
},
551
552
renderHTML({ mark, HTMLAttributes }) {
553
return [
554
'a',
555
{
556
href: mark.attrs.href,
557
target: mark.attrs.target,
558
rel: mark.attrs.rel,
559
...HTMLAttributes,
560
},
561
0,
562
];
563
},
564
565
addCommands() {
566
return {
567
setLink: (attributes: { href: string; target?: string; rel?: string }) => ({ commands }) => {
568
return commands.setMark(this.name, attributes);
569
},
570
571
toggleLink: (attributes: { href: string; target?: string; rel?: string }) => ({ commands }) => {
572
return commands.toggleMark(this.name, attributes);
573
},
574
575
unsetLink: () => ({ commands }) => {
576
return commands.unsetMark(this.name);
577
},
578
};
579
},
580
581
// Handle exit when typing at end of link
582
onExit() {
583
return this.editor.commands.unsetMark(this.name);
584
},
585
});
586
587
// Use static handleExit for complex exit behavior
588
Mark.handleExit({ editor, mark: linkMark });
589
```
590
591
### Node and Mark Views
592
593
Custom rendering for nodes and marks using framework components or custom DOM manipulation.
594
595
```typescript { .api }
596
/**
597
* Node view renderer function type
598
*/
599
type NodeViewRenderer = (props: {
600
editor: Editor;
601
node: ProseMirrorNode;
602
getPos: () => number;
603
HTMLAttributes: Record<string, any>;
604
decorations: readonly Decoration[];
605
extension: Node;
606
}) => NodeView;
607
608
/**
609
* Mark view renderer function type
610
*/
611
type MarkViewRenderer = (props: {
612
editor: Editor;
613
mark: ProseMirrorMark;
614
HTMLAttributes: Record<string, any>;
615
extension: Mark;
616
}) => MarkView;
617
618
interface NodeView {
619
dom: HTMLElement;
620
contentDOM?: HTMLElement | null;
621
update?(node: ProseMirrorNode, decorations: readonly Decoration[]): boolean;
622
selectNode?(): void;
623
deselectNode?(): void;
624
setSelection?(anchor: number, head: number, root: Document | ShadowRoot): void;
625
stopEvent?(event: Event): boolean;
626
ignoreMutation?(record: MutationRecord): boolean | void;
627
destroy?(): void;
628
}
629
630
interface MarkView {
631
dom: HTMLElement;
632
contentDOM?: HTMLElement | null;
633
update?(mark: ProseMirrorMark): boolean;
634
destroy?(): void;
635
}
636
```
637
638
**Usage Examples:**
639
640
```typescript
641
// Custom node view
642
const CustomParagraphNode = Node.create({
643
name: 'customParagraph',
644
group: 'block',
645
content: 'inline*',
646
647
addNodeView() {
648
return ({ node, getPos, editor }) => {
649
const dom = document.createElement('div');
650
const contentDOM = document.createElement('p');
651
652
dom.className = 'custom-paragraph-wrapper';
653
contentDOM.className = 'custom-paragraph-content';
654
655
// Add custom controls
656
const controls = document.createElement('div');
657
controls.className = 'paragraph-controls';
658
controls.innerHTML = '<button>Edit</button>';
659
660
dom.appendChild(controls);
661
dom.appendChild(contentDOM);
662
663
return {
664
dom,
665
contentDOM,
666
update: (newNode) => {
667
return newNode.type === node.type;
668
},
669
selectNode: () => {
670
dom.classList.add('ProseMirror-selectednode');
671
},
672
deselectNode: () => {
673
dom.classList.remove('ProseMirror-selectednode');
674
}
675
};
676
};
677
}
678
});
679
680
// Custom mark view
681
const CustomEmphasisMark = Mark.create({
682
name: 'customEmphasis',
683
684
addMarkView() {
685
return ({ mark, HTMLAttributes }) => {
686
const dom = document.createElement('em');
687
const contentDOM = document.createElement('span');
688
689
dom.className = 'custom-emphasis';
690
contentDOM.className = 'emphasis-content';
691
692
dom.appendChild(contentDOM);
693
694
return {
695
dom,
696
contentDOM,
697
update: (newMark) => {
698
return newMark.type === mark.type;
699
}
700
};
701
};
702
}
703
});
704
```