Skip to main content

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 {}
  },
})
Source: 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,
      }),
    ]
  },
})
Source: 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,
      }),
    ]
  },
})
Source: 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,
      }),
    ]
  },
})
Source: 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,
})
Source: 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,
        }),
      },
    }
  },
})
Source: 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 
      }),
    }
  },
})
Source: 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
})
Source: 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)
      },
    }
  },
})
Source: 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(),
    }
  },
})
Source: 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

Build docs developers (and LLMs) love