> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/ueberdosis/tiptap/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Nodes

> Learn how to create custom nodes to add block-level content to Tiptap

# Custom Nodes

Nodes are the building blocks for block-level content in Tiptap. They represent things like paragraphs, headings, blockquotes, code blocks, and more. Each node can have attributes, content rules, and custom rendering.

## Understanding Nodes

The Node class extends the Extendable class and provides specific functionality for block-level content.

<CodeGroup>
  ```typescript Node API theme={null}
  import { Node } from '@tiptap/core'

  export const MyNode = Node.create({
    name: 'myNode',
    
    // Define content structure
    content: 'inline*',
    group: 'block',
    
    // Parse from HTML
    parseHTML() {
      return [{ tag: 'div' }]
    },
    
    // Render to HTML
    renderHTML({ HTMLAttributes }) {
      return ['div', HTMLAttributes, 0]
    },
    
    // Add attributes
    addAttributes() {
      return {}
    },
    
    // Add commands
    addCommands() {
      return {}
    },
  })
  ```
</CodeGroup>

Source: `packages/core/src/Node.ts:340`

## Creating a Simple Node

Let's start with a simple paragraph node.

<CodeGroup>
  ```typescript Paragraph Node theme={null}
  import { Node, mergeAttributes } from '@tiptap/core'

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

  export const Paragraph = Node.create<ParagraphOptions>({
    name: 'paragraph',
    
    priority: 1000,
    
    addOptions() {
      return {
        HTMLAttributes: {},
      }
    },
    
    group: 'block',
    
    content: 'inline*',
    
    parseHTML() {
      return [{ tag: 'p' }]
    },
    
    renderHTML({ HTMLAttributes }) {
      return ['p', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
    },
    
    addCommands() {
      return {
        setParagraph: () => ({ commands }) => {
          return commands.setNode(this.name)
        },
      }
    },
    
    addKeyboardShortcuts() {
      return {
        'Mod-Alt-0': () => this.editor.commands.setParagraph(),
      }
    },
  })
  ```
</CodeGroup>

Source: `packages/extension-paragraph/src/paragraph.ts:41`

## Node with Attributes

Here's a heading node that demonstrates attributes.

<CodeGroup>
  ```typescript Heading Node theme={null}
  import { Node, mergeAttributes, textblockTypeInputRule } from '@tiptap/core'

  export type Level = 1 | 2 | 3 | 4 | 5 | 6

  export interface HeadingOptions {
    levels: Level[]
    HTMLAttributes: Record<string, any>
  }

  export const Heading = Node.create<HeadingOptions>({
    name: 'heading',
    
    addOptions() {
      return {
        levels: [1, 2, 3, 4, 5, 6],
        HTMLAttributes: {},
      }
    },
    
    content: 'inline*',
    group: 'block',
    defining: true,
    
    addAttributes() {
      return {
        level: {
          default: 1,
          rendered: false,
        },
      }
    },
    
    parseHTML() {
      return this.options.levels.map((level: Level) => ({
        tag: `h${level}`,
        attrs: { level },
      }))
    },
    
    renderHTML({ node, HTMLAttributes }) {
      const hasLevel = this.options.levels.includes(node.attrs.level)
      const level = hasLevel ? node.attrs.level : this.options.levels[0]
      
      return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
    },
    
    addCommands() {
      return {
        setHeading: (attributes) => ({ commands }) => {
          if (!this.options.levels.includes(attributes.level)) {
            return false
          }
          return commands.setNode(this.name, attributes)
        },
        toggleHeading: (attributes) => ({ commands }) => {
          if (!this.options.levels.includes(attributes.level)) {
            return false
          }
          return commands.toggleNode(this.name, 'paragraph', attributes)
        },
      }
    },
    
    addKeyboardShortcuts() {
      return this.options.levels.reduce(
        (items, level) => ({
          ...items,
          [`Mod-Alt-${level}`]: () => this.editor.commands.toggleHeading({ level }),
        }),
        {},
      )
    },
    
    addInputRules() {
      return this.options.levels.map(level => {
        return textblockTypeInputRule({
          find: new RegExp(`^(#{${Math.min(...this.options.levels)},${level}})\\s$`),
          type: this.type,
          getAttributes: { level },
        })
      })
    },
  })
  ```
</CodeGroup>

Source: `packages/extension-heading/src/heading.ts:47`

## Wrapping Nodes

Some nodes wrap other content, like blockquotes.

<CodeGroup>
  ```typescript Blockquote Node theme={null}
  import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core'

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

  export const Blockquote = Node.create<BlockquoteOptions>({
    name: 'blockquote',
    
    addOptions() {
      return {
        HTMLAttributes: {},
      }
    },
    
    content: 'block+',
    group: 'block',
    defining: true,
    
    parseHTML() {
      return [{ tag: 'blockquote' }]
    },
    
    renderHTML({ HTMLAttributes }) {
      return ['blockquote', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
    },
    
    addCommands() {
      return {
        setBlockquote: () => ({ commands }) => {
          return commands.wrapIn(this.name)
        },
        toggleBlockquote: () => ({ commands }) => {
          return commands.toggleWrap(this.name)
        },
        unsetBlockquote: () => ({ commands }) => {
          return commands.lift(this.name)
        },
      }
    },
    
    addKeyboardShortcuts() {
      return {
        'Mod-Shift-b': () => this.editor.commands.toggleBlockquote(),
      }
    },
    
    addInputRules() {
      return [
        wrappingInputRule({
          find: /^\s*>\s$/,
          type: this.type,
        }),
      ]
    },
  })
  ```
</CodeGroup>

Source: `packages/extension-blockquote/src/blockquote.tsx:41`

## Content Expressions

Content expressions define what content a node can contain.

<CodeGroup>
  ```typescript Content Expressions theme={null}
  // No content (leaf node)
  content: ''

  // Inline content (like paragraph)
  content: 'inline*'

  // One or more block nodes
  content: 'block+'

  // Specific node types
  content: 'heading paragraph+'

  // Alternation
  content: '(paragraph | heading)+'

  // Optional content
  content: 'heading? paragraph+'
  ```
</CodeGroup>

Source: `packages/core/src/Node.ts:32`

## Node Groups

Groups allow you to reference multiple node types in content expressions.

<CodeGroup>
  ```typescript Node Groups theme={null}
  import { Node } from '@tiptap/core'

  // Belongs to 'block' group
  export const Paragraph = Node.create({
    name: 'paragraph',
    group: 'block',
    content: 'inline*',
  })

  // Belongs to 'inline' group
  export const HardBreak = Node.create({
    name: 'hardBreak',
    group: 'inline',
    inline: true,
  })

  // Custom group
  export const CustomBlock = Node.create({
    name: 'customBlock',
    group: 'customGroup',
    content: 'block+',
  })
  ```
</CodeGroup>

Source: `packages/core/src/Node.ts:72`

## Node Attributes

Attributes store data on nodes and control rendering.

<CodeGroup>
  ```typescript Node Attributes theme={null}
  import { Node } from '@tiptap/core'

  export const MyNode = Node.create({
    name: 'myNode',
    
    addAttributes() {
      return {
        // Simple attribute with default
        id: {
          default: null,
        },
        
        // Attribute with parsing and rendering
        color: {
          default: 'black',
          parseHTML: element => element.getAttribute('data-color'),
          renderHTML: attributes => {
            if (!attributes.color) {
              return {}
            }
            return {
              'data-color': attributes.color,
              style: `color: ${attributes.color}`,
            }
          },
        },
        
        // Don't render in HTML
        internalState: {
          default: null,
          rendered: false,
        },
        
        // Keep attribute on split
        keepOnSplit: {
          default: false,
          keepOnSplit: true,
        },
      }
    },
  })
  ```
</CodeGroup>

Source: `packages/core/src/Node.ts:323`

## Node Properties

Nodes have several important properties that control their behavior.

<CodeGroup>
  ```typescript Node Properties theme={null}
  import { Node } from '@tiptap/core'

  export const MyNode = Node.create({
    name: 'myNode',
    
    // Top-level node (like document)
    topNode: false,
    
    // Inline node (flows with text)
    inline: false,
    
    // Treat as single unit (like image)
    atom: false,
    
    // Can be selected as node
    selectable: true,
    
    // Can be dragged
    draggable: false,
    
    // Contains code
    code: false,
    
    // Whitespace handling
    whitespace: 'normal', // or 'pre'
    
    // Defines context boundaries
    defining: false,
    
    // Creates editing boundaries
    isolating: false,
  })
  ```
</CodeGroup>

Source: `packages/core/src/Node.ts:25`

## Input Rules

Input rules allow you to trigger node creation from text patterns.

<CodeGroup>
  ```typescript Node Input Rules theme={null}
  import { Node, textblockTypeInputRule, wrappingInputRule } from '@tiptap/core'

  // Create a text block node
  export const Heading = Node.create({
    name: 'heading',
    
    addInputRules() {
      return [
        textblockTypeInputRule({
          find: /^(#{1,6})\s$/,
          type: this.type,
          getAttributes: match => ({ level: match[1].length }),
        }),
      ]
    },
  })

  // Wrap content in a node
  export const Blockquote = Node.create({
    name: 'blockquote',
    
    addInputRules() {
      return [
        wrappingInputRule({
          find: /^>\s$/,
          type: this.type,
        }),
      ]
    },
  })
  ```
</CodeGroup>

## TypeScript Commands

Add type-safe commands to your node.

<CodeGroup>
  ```typescript Typed Commands theme={null}
  import { Node } from '@tiptap/core'

  export interface HeadingOptions {
    levels: Level[]
  }

  export type Level = 1 | 2 | 3 | 4 | 5 | 6

  declare module '@tiptap/core' {
    interface Commands<ReturnType> {
      heading: {
        setHeading: (attributes: { level: Level }) => ReturnType
        toggleHeading: (attributes: { level: Level }) => ReturnType
      }
    }
  }

  export const Heading = Node.create<HeadingOptions>({
    name: 'heading',
    
    addCommands() {
      return {
        setHeading: (attributes) => ({ commands }) => {
          return commands.setNode(this.name, attributes)
        },
        toggleHeading: (attributes) => ({ commands }) => {
          return commands.toggleNode(this.name, 'paragraph', attributes)
        },
      }
    },
  })
  ```
</CodeGroup>

Source: `packages/extension-heading/src/heading.ts:24`

## Extending Nodes

You can extend existing nodes to customize behavior.

<CodeGroup>
  ```typescript Extend Node theme={null}
  import { Paragraph } from '@tiptap/extension-paragraph'

  export const CustomParagraph = Paragraph.extend({
    addAttributes() {
      return {
        ...this.parent?.(),
        class: {
          default: 'custom-paragraph',
          parseHTML: element => element.getAttribute('class'),
          renderHTML: attributes => ({
            class: attributes.class,
          }),
        },
      }
    },
  })
  ```
</CodeGroup>

Source: `packages/core/src/Node.ts:357`

<Warning>
  Nodes must have unique names within the schema. Make sure your custom node name doesn't conflict with built-in nodes.
</Warning>

## Next Steps

<CardGroup cols={2}>
  <Card title="Custom Marks" icon="highlighter" href="/guides/custom-marks">
    Create custom inline formatting
  </Card>

  <Card title="Custom Extensions" icon="puzzle-piece" href="/guides/custom-extensions">
    Add editor-wide functionality
  </Card>
</CardGroup>
