Custom Marks
Marks are inline styles that can be applied to text, like bold, italic, underline, or links. Unlike nodes which are structural, marks are formatting that can be toggled on and off for portions of text.Understanding Marks
The Mark class extends the Extendable class and provides functionality for inline formatting.import { Mark } from '@tiptap/core'
export const MyMark = Mark.create({
name: 'myMark',
// Parse from HTML
parseHTML() {
return [{ tag: 'span' }]
},
// Render to HTML
renderHTML({ HTMLAttributes }) {
return ['span', HTMLAttributes, 0]
},
// Add attributes
addAttributes() {
return {}
},
// Add commands
addCommands() {
return {}
},
})
packages/core/src/Mark.ts:146
Creating a Simple Mark
Let’s start with the bold mark.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
}
}
}
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=400',
clearMark: mark => mark.type.name === this.name,
},
{
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 }) => {
return commands.setMark(this.name)
},
toggleBold: () => ({ commands }) => {
return commands.toggleMark(this.name)
},
unsetBold: () => ({ commands }) => {
return commands.unsetMark(this.name)
},
}
},
addKeyboardShortcuts() {
return {
'Mod-b': () => this.editor.commands.toggleBold(),
'Mod-B': () => this.editor.commands.toggleBold(),
}
},
addInputRules() {
return [
markInputRule({
find: /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))$/,
type: this.type,
}),
markInputRule({
find: /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))$/,
type: this.type,
}),
]
},
addPasteRules() {
return [
markPasteRule({
find: /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))/g,
type: this.type,
}),
markPasteRule({
find: /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g,
type: this.type,
}),
]
},
})
packages/extension-bold/src/bold.tsx:56
Mark with Attributes
Here’s the highlight mark that demonstrates using attributes.import { Mark, markInputRule, markPasteRule, mergeAttributes } from '@tiptap/core'
export interface HighlightOptions {
multicolor: boolean
HTMLAttributes: Record<string, any>
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
highlight: {
setHighlight: (attributes?: { color: string }) => ReturnType
toggleHighlight: (attributes?: { color: string }) => ReturnType
unsetHighlight: () => ReturnType
}
}
}
export const Highlight = Mark.create<HighlightOptions>({
name: 'highlight',
addOptions() {
return {
multicolor: false,
HTMLAttributes: {},
}
},
addAttributes() {
if (!this.options.multicolor) {
return {}
}
return {
color: {
default: null,
parseHTML: element =>
element.getAttribute('data-color') || element.style.backgroundColor,
renderHTML: attributes => {
if (!attributes.color) {
return {}
}
return {
'data-color': attributes.color,
style: `background-color: ${attributes.color}; color: inherit`,
}
},
},
}
},
parseHTML() {
return [{ tag: 'mark' }]
},
renderHTML({ HTMLAttributes }) {
return ['mark', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
},
addCommands() {
return {
setHighlight: (attributes) => ({ commands }) => {
return commands.setMark(this.name, attributes)
},
toggleHighlight: (attributes) => ({ commands }) => {
return commands.toggleMark(this.name, attributes)
},
unsetHighlight: () => ({ commands }) => {
return commands.unsetMark(this.name)
},
}
},
addKeyboardShortcuts() {
return {
'Mod-Shift-h': () => this.editor.commands.toggleHighlight(),
}
},
addInputRules() {
return [
markInputRule({
find: /(?:^|\s)(==(?!\s+==)((?:[^=]+))==(?!\s+==))$/,
type: this.type,
}),
]
},
addPasteRules() {
return [
markPasteRule({
find: /(?:^|\s)(==(?!\s+==)((?:[^=]+))==(?!\s+==))/g,
type: this.type,
}),
]
},
})
packages/extension-highlight/src/highlight.ts:57
Simple Italic Mark
The italic mark shows a minimal implementation.import { Mark, markInputRule, markPasteRule, mergeAttributes } from '@tiptap/core'
export interface ItalicOptions {
HTMLAttributes: Record<string, any>
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
italic: {
setItalic: () => ReturnType
toggleItalic: () => ReturnType
unsetItalic: () => ReturnType
}
}
}
export const Italic = Mark.create<ItalicOptions>({
name: 'italic',
addOptions() {
return {
HTMLAttributes: {},
}
},
parseHTML() {
return [
{ tag: 'em' },
{
tag: 'i',
getAttrs: node => (node as HTMLElement).style.fontStyle !== 'normal' && null
},
{
style: 'font-style=normal',
clearMark: mark => mark.type.name === this.name,
},
{
style: 'font-style=italic',
},
]
},
renderHTML({ HTMLAttributes }) {
return ['em', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
},
addCommands() {
return {
setItalic: () => ({ commands }) => {
return commands.setMark(this.name)
},
toggleItalic: () => ({ commands }) => {
return commands.toggleMark(this.name)
},
unsetItalic: () => ({ commands }) => {
return commands.unsetMark(this.name)
},
}
},
addKeyboardShortcuts() {
return {
'Mod-i': () => this.editor.commands.toggleItalic(),
'Mod-I': () => this.editor.commands.toggleItalic(),
}
},
addInputRules() {
return [
markInputRule({
find: /(?:^|\s)(\*(?!\s+\*)((?:[^*]+))\*(?!\s+\*))$/,
type: this.type,
}),
markInputRule({
find: /(?:^|\s)(_(?!\s+_)((?:[^_]+))_(?!\s+_))$/,
type: this.type,
}),
]
},
addPasteRules() {
return [
markPasteRule({
find: /(?:^|\s)(\*(?!\s+\*)((?:[^*]+))\*(?!\s+\*))/g,
type: this.type,
}),
markPasteRule({
find: /(?:^|\s)(_(?!\s+_)((?:[^_]+))_(?!\s+_))/g,
type: this.type,
}),
]
},
})
packages/extension-italic/src/italic.ts:58
Mark Properties
Marks have several important properties that control their behavior.import { Mark } from '@tiptap/core'
export const MyMark = Mark.create({
name: 'myMark',
// Mark remains after splitting node (like bold)
keepOnSplit: true,
// Mark is inclusive at boundaries
inclusive: true,
// Can be exited with arrow keys
exitable: true,
// This mark excludes other marks
excludes: 'link code',
// Belongs to mark group
group: 'formatting',
// Mark spans across nodes
spanning: true,
// Represents code
code: false,
})
packages/core/src/Mark.ts:25
Mark Attributes
Attributes allow marks to store additional data.import { Mark } from '@tiptap/core'
export const Link = Mark.create({
name: 'link',
addAttributes() {
return {
href: {
default: null,
parseHTML: element => element.getAttribute('href'),
renderHTML: attributes => {
if (!attributes.href) {
return {}
}
return { href: attributes.href }
},
},
target: {
default: '_blank',
parseHTML: element => element.getAttribute('target'),
renderHTML: attributes => ({
target: attributes.target,
rel: 'noopener noreferrer nofollow',
}),
},
class: {
default: 'link',
parseHTML: element => element.getAttribute('class'),
renderHTML: attributes => ({
class: attributes.class,
}),
},
}
},
})
packages/core/src/Mark.ts:130
Mark Input Rules
Input rules automatically apply marks based on text patterns.import { Mark, markInputRule } from '@tiptap/core'
export const Bold = Mark.create({
name: 'bold',
addInputRules() {
return [
// Matches **bold** syntax
markInputRule({
find: /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))$/,
type: this.type,
}),
// Matches __bold__ syntax
markInputRule({
find: /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))$/,
type: this.type,
}),
]
},
})
Mark Paste Rules
Paste rules apply marks when content is pasted.import { Mark, markPasteRule } from '@tiptap/core'
export const Bold = Mark.create({
name: 'bold',
addPasteRules() {
return [
markPasteRule({
find: /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))/g,
type: this.type,
}),
markPasteRule({
find: /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g,
type: this.type,
}),
]
},
})
Exitable Marks
Some marks should be easy to exit with arrow keys.import { Mark } from '@tiptap/core'
export const Link = Mark.create({
name: 'link',
exitable: true,
addKeyboardShortcuts() {
return {
// Exit link with arrow right at end
ArrowRight: () => Mark.handleExit({
editor: this.editor,
mark: this
}),
}
},
})
packages/core/src/Mark.ts:159
Mark Exclusion
Some marks cannot coexist with others.import { Mark } from '@tiptap/core'
// Code mark excludes all other formatting
export const Code = Mark.create({
name: 'code',
excludes: '_', // Excludes all marks
})
// Link excludes other links
export const Link = Mark.create({
name: 'link',
excludes: 'link', // Can't nest links
})
// Custom exclusion
export const Highlight = Mark.create({
name: 'highlight',
excludes: 'highlight link', // Excludes highlight and link
})
packages/core/src/Mark.ts:42
TypeScript Commands
Add type-safe commands to your mark.import { Mark } from '@tiptap/core'
export interface HighlightOptions {
multicolor: boolean
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
highlight: {
setHighlight: (attributes?: { color: string }) => ReturnType
toggleHighlight: (attributes?: { color: string }) => ReturnType
unsetHighlight: () => ReturnType
}
}
}
export const Highlight = Mark.create<HighlightOptions>({
name: 'highlight',
addCommands() {
return {
setHighlight: (attributes) => ({ commands }) => {
return commands.setMark(this.name, attributes)
},
toggleHighlight: (attributes) => ({ commands }) => {
return commands.toggleMark(this.name, attributes)
},
unsetHighlight: () => ({ commands }) => {
return commands.unsetMark(this.name)
},
}
},
})
packages/extension-highlight/src/highlight.ts:19
Extending Marks
You can extend existing marks to customize behavior.import { Bold } from '@tiptap/extension-bold'
export const CustomBold = Bold.extend({
addOptions() {
return {
...this.parent?.(),
HTMLAttributes: {
class: 'custom-bold',
},
}
},
addKeyboardShortcuts() {
return {
...this.parent?.(),
'Mod-Shift-b': () => this.editor.commands.toggleBold(),
}
},
})
packages/core/src/Mark.ts:191
Marks are ideal for inline formatting that can be toggled on and off. Use Nodes for block-level content and Extensions for editor-wide functionality.
Next Steps
Custom Nodes
Create custom block content
Styling
Style your editor and content