Skip to main content

Creating a Node

Use the static create method to define a new node.
import { Node } from '@tiptap/core'

const MyNode = Node.create<Options, Storage>({
  name: 'myNode',
  // ... configuration
})
config
Partial<NodeConfig<Options, Storage>> | (() => Partial<NodeConfig<Options, Storage>>)
The node configuration object or a function that returns a configuration object.

Configuration Options

Nodes inherit all configuration options from Extension, plus the following:

content

Define the content expression for the node.
content?: string | ((this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['content']
  editor?: Editor
}) => string)
content
string
A ProseMirror content expression defining what content this node can contain.
Examples
// Block nodes only
content: 'block+'

// Inline content
content: 'inline*'

// Specific nodes
content: 'heading paragraph block*'

// No content (leaf node)
content: undefined

marks

Define which marks are allowed inside the node.
marks?: string | ((this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['marks']
  editor?: Editor
}) => string)
marks
string
Space-separated mark names, "_" to allow all marks, or "" to disallow marks.
Example
// Allow specific marks
marks: 'strong em'

// Allow all marks
marks: '_'

// Disallow all marks
marks: ''

group

Define the group(s) this node belongs to.
group?: string | ((this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['group']
  editor?: Editor
}) => string)
group
string
Space-separated group names (e.g., 'block', 'inline').
Example
group: 'block'

// Multiple groups
group: 'block customGroup'

inline

Whether the node is an inline node.
inline?: boolean | ((this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['inline']
  editor?: Editor
}) => boolean)
inline
boolean
default:"false"
Set to true for inline nodes.
Example
inline: true

atom

Whether the node is atomic (non-editable as a unit).
atom?: boolean | ((this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['atom']
  editor?: Editor
}) => boolean)
atom
boolean
default:"false"
Set to true for atomic nodes that should be treated as a single unit.
Example
atom: true

selectable

Whether the node can be selected.
selectable?: boolean | ((this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['selectable']
  editor?: Editor
}) => boolean)
selectable
boolean
default:"true"
Whether the node can be selected with a node selection.
Example
selectable: false

draggable

Whether the node can be dragged.
draggable?: boolean | ((this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['draggable']
  editor?: Editor
}) => boolean)
draggable
boolean
default:"false"
Whether the node can be dragged without being selected.
Example
draggable: true

code

Whether the node contains code.
code?: boolean | ((this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['code']
  editor?: Editor
}) => boolean)
code
boolean
default:"false"
Indicates that this node contains code, affecting command behavior.
Example
code: true

defining

Whether the node is defining.
defining?: boolean | ((this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['defining']
  editor?: Editor
}) => boolean)
defining
boolean
default:"false"
When enabled, affects context for schema operations.

isolating

Whether the node is isolating.
isolating?: boolean | ((this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['isolating']
  editor?: Editor
}) => boolean)
isolating
boolean
default:"false"
When enabled, the sides of this node count as boundaries that regular editing operations won’t cross.
Example
isolating: true // e.g., for table cells

topNode

Whether this node should be the top-level node (document).
topNode?: boolean
topNode
boolean
default:"false"
Set to true for the document node.
Example
topNode: true

parseHTML()

Define how to parse HTML into this node.
parseHTML?(this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['parseHTML']
  editor?: Editor
}): ParseRule[]
Example
parseHTML() {
  return [
    { tag: 'p' },
    { tag: 'div', getAttrs: node => (node as HTMLElement).classList.contains('paragraph') && null },
  ]
}

renderHTML()

Define how to render the node as HTML.
renderHTML?(this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['renderHTML']
  editor?: Editor
}, props: {
  node: ProseMirrorNode
  HTMLAttributes: Record<string, any>
}): DOMOutputSpec
Example
renderHTML({ node, HTMLAttributes }) {
  return ['p', HTMLAttributes, 0]
}

// With custom attributes
renderHTML({ node, HTMLAttributes }) {
  return [
    'div',
    { ...HTMLAttributes, class: 'my-node', 'data-id': node.attrs.id },
    0, // 0 represents where content goes
  ]
}

renderText()

Define how to render the node as plain text.
renderText?(this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['renderText']
  editor?: Editor
}, props: {
  node: ProseMirrorNode
  pos: number
  parent: ProseMirrorNode
  index: number
}): string
Example
renderText({ node }) {
  return node.textContent
}

addAttributes()

Define attributes for the node.
addAttributes?(this: {
  name: string
  options: Options
  storage: Storage
  parent: ParentConfig['addAttributes']
  editor?: Editor
}): Attributes | {}
Example
addAttributes() {
  return {
    level: {
      default: 1,
      rendered: true,
      parseHTML: element => element.getAttribute('data-level'),
      renderHTML: attributes => {
        return { 'data-level': attributes.level }
      },
    },
    id: {
      default: null,
      parseHTML: element => element.getAttribute('id'),
      renderHTML: attributes => {
        if (!attributes.id) return {}
        return { id: attributes.id }
      },
    },
  }
}

addNodeView()

Define a custom node view.
addNodeView?(this: {
  name: string
  options: Options
  storage: Storage
  editor: Editor
  type: NodeType
  parent: ParentConfig['addNodeView']
}): NodeViewRenderer | null
Example
import { NodeViewWrapper } from '@tiptap/react'

addNodeView() {
  return ({ node, getPos, editor }) => {
    const dom = document.createElement('div')
    dom.classList.add('my-custom-node')
    dom.textContent = node.textContent
    
    return {
      dom,
      contentDOM: dom,
      update: (updatedNode) => {
        if (updatedNode.type !== node.type) return false
        // Update view
        return true
      },
      destroy: () => {
        // Cleanup
      },
    }
  }
}

Methods

configure()

Create a configured version of the node.
node.configure(options?: Partial<Options>): Node<Options, Storage>
options
Partial<Options>
Options to override the default options.
Example
import Heading from '@tiptap/extension-heading'

const editor = new Editor({
  extensions: [
    Heading.configure({
      levels: [1, 2, 3],
    }),
  ],
})

extend()

Extend the node with additional configuration.
node.extend<ExtendedOptions, ExtendedStorage, ExtendedConfig>(
  extendedConfig?: Partial<ExtendedConfig> | (() => Partial<ExtendedConfig>)
): Node<ExtendedOptions, ExtendedStorage>
extendedConfig
Partial<ExtendedConfig> | (() => Partial<ExtendedConfig>)
Additional configuration or a function that returns configuration.
Example
import Heading from '@tiptap/extension-heading'

const CustomHeading = Heading.extend({
  addAttributes() {
    return {
      ...this.parent?.(),
      customId: {
        default: null,
        parseHTML: element => element.getAttribute('data-custom-id'),
        renderHTML: attributes => {
          if (!attributes.customId) return {}
          return { 'data-custom-id': attributes.customId }
        },
      },
    }
  },
})

Complete Example

import { Node, mergeAttributes } from '@tiptap/core'

interface ImageOptions {
  inline: boolean
  allowBase64: boolean
  HTMLAttributes: Record<string, any>
}

const Image = Node.create<ImageOptions>({
  name: 'image',

  addOptions() {
    return {
      inline: false,
      allowBase64: false,
      HTMLAttributes: {},
    }
  },

  inline() {
    return this.options.inline
  },

  group() {
    return this.options.inline ? 'inline' : 'block'
  },

  draggable: true,

  addAttributes() {
    return {
      src: {
        default: null,
        parseHTML: element => element.getAttribute('src'),
        renderHTML: attributes => {
          if (!attributes.src) return {}
          return { src: attributes.src }
        },
      },
      alt: {
        default: null,
        parseHTML: element => element.getAttribute('alt'),
        renderHTML: attributes => {
          if (!attributes.alt) return {}
          return { alt: attributes.alt }
        },
      },
      title: {
        default: null,
        parseHTML: element => element.getAttribute('title'),
        renderHTML: attributes => {
          if (!attributes.title) return {}
          return { title: attributes.title }
        },
      },
    }
  },

  parseHTML() {
    return [
      {
        tag: this.options.allowBase64 ? 'img[src]' : 'img[src]:not([src^="data:"])',
      },
    ]
  },

  renderHTML({ HTMLAttributes }) {
    return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
  },

  addCommands() {
    return {
      setImage: (options) => ({ commands }) => {
        return commands.insertContent({
          type: this.name,
          attrs: options,
        })
      },
    }
  },
})

// Usage
const editor = new Editor({
  extensions: [
    Image.configure({
      inline: true,
      allowBase64: true,
    }),
  ],
})

editor.commands.setImage({ src: 'image.jpg', alt: 'Description' })

Build docs developers (and LLMs) love