Find out what's new in Tiptap V3

Node API

The power of Tiptap lies in its flexibility. You can create your own extensions from scratch and build a unique editor experience tailored to your needs.

Creating a node

Nodes are the building blocks of your editor. They can be blocks or inline nodes. Good examples to learn from are Paragraph, Heading, or CodeBlock.

They extend all of the options and methods from the Extension API and add a few more specific to nodes.

Let's add a simple node extension to see how it works.

import { Node } from '@tiptap/core'

const CustomNode = Node.create({
  name: 'customNode',

  addOptions() {
    return {
      HTMLAttributes: {},
    }
  },

  parseHTML() {
    return [
      {
        tag: 'div',
      },
    ]
  },

  renderHTML({ HTMLAttributes }) {
    return ['div', HTMLAttributes, 0]
  },
})

You can also use a callback function to create a node. This is useful if you want to encapsulate the logic of your extension, for example when you want to define event handlers or other custom logic.

import { Node } from '@tiptap/core'

const CustomNode = Node.create(() => {
  // here you could define variables or function that you can use on your schema definition
  const customVariable = 'foo'

  function onCreate() {}
  function onUpdate() {}

  return {
    name: 'customNode',
    onCreate,
    onUpdate,

    // Your code goes here.
  }
})

This code creates a new node extension named CustomNode. It adds an addOptions method to define the node's options, which are configurable by the user. It also adds parseHTML and renderHTML methods to define how the node is parsed and rendered as HTML.

It is installed to the editor just like any other extension by adding it to the extensions array.

import { Editor } from '@tiptap/core'

new Editor({
  extensions: [CustomNode],
})

// Or if using React or Vue

const editor = useEditor({
  extensions: [CustomNode],
})

Now let's take a closer look at the options and methods available for nodes.

Node options

When creating a node, you can define options that are configurable by the user. These options can be used to customize the behavior or appearance of the node.

parseHTML

The parseHTML method is used to define how the mark is parsed from HTML. It should return an array of objects representing the mark's attributes.

Maps to the parseDOM attribute in the ProseMirror schema.

const CustomMark = Mark.create({
  name: 'customMark',

  parseHTML() {
    return [
      {
        tag: 'span',
        getAttrs: (node) => {
          return {
            class: node.getAttribute('class'),
          }
        },
      },
    ]
  },
})

This will be used during paste events to parse the HTML content into a mark.

renderHTML

The renderHTML method is used to define how the mark is rendered as HTML. It should return an array representing the mark's HTML representation.

Maps to the toDOM attribute in the ProseMirror schema.

const CustomMark = Mark.create({
  name: 'customMark',

  renderHTML({ HTMLAttributes }) {
    return ['span', HTMLAttributes, 0]
  },
})

This will be used during copy events to render the mark as HTML. For more details, see the extend existing extensions guide.

addAttributes

The addAttributes method is used to define custom attributes for the mark. It should return an object with the attribute names and their default values.

Maps to the attrs attribute in the ProseMirror schema.

const CustomMark = Mark.create({
  name: 'customMark',

  addAttributes() {
    return {
      customAttribute: {
        default: 'value',
        parseHTML: (element) => element.getAttribute('data-custom-attribute'),
      },
    }
  },
})

For more details, see the extend existing extensions guide.

topNode

Defines if this node should be a top-level node (doc).

Maps to the topNode attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  topNode: true,
})

content

The content expression for this node, as described in the schema guide. When not given, the node does not allow any content.

You can read more about it on the Prosemirror documentation here.

const CustomNode = Node.create({
  name: 'customNode',

  content: 'block+',
})

marks

The marks that are allowed inside of this node. May be a space-separated string referring to mark names or groups, "_" to explicitly allow all marks, or "" to disallow marks. When not given, nodes with inline content default to allowing all marks, other nodes default to not allowing marks.

Maps to the marks attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  marks: 'strong em',
})

group

The group or space-separated groups to which this node belongs, which can be referred to in the content expressions for the schema.

By default, Tiptap uses the groups 'block' and 'inline' for nodes. You can also use custom groups if you want to group specific nodes together and handle them in your schema.

Maps to the group attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  group: 'block',
})

inline

Should be set to true for inline nodes. (Implied for text nodes).

Maps to the inline attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  inline: true,
})

atom

Can be set to true to indicate that, though this isn't a leaf node, it doesn't have directly editable content and should be treated as a single unit in the view.

Maps to the atom attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  atom: true,
})

selectable

Controls whether nodes of this type can be selected as a node selection. Defaults to true for non-text nodes.

Maps to the selectable attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  selectable: false,
})

draggable

Determines whether nodes of this type can be dragged without being selected. Defaults to false.

Maps to the draggable attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  draggable: true,
})

code

Can be used to indicate that this node contains code, which causes some commands to behave differently.

Maps to the code attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  code: true,
})

whitespace

Controls the way whitespace in this a node is parsed. The default is "normal", which causes the DOM parser to collapse whitespace in normal mode, and normalize it (replacing newlines and such with spaces) otherwise. "pre" causes the parser to preserve spaces inside the node. When this option isn't given, but code is true, whitespace will default to "pre".

Maps to the whitespace attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  whitespace: 'pre',
})

linebreakReplacement

Allows a single node to be set as a linebreak equivalent (e.g. hardBreak). When converting between block types that have whitespace set to "pre" and don't support the linebreak node (e.g. codeBlock) and other block types that do support the linebreak node (e.g. paragraphs) - this node will be used as the linebreak instead of stripping the newline.

Maps to the linebreakReplacement attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  linebreakReplacement: true,
})

defining

When enabled, enables both definingAsContext and definingForContent.

Maps to the defining attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  defining: true,
})

isolating

When enabled (default is false), the sides of nodes of this type count as boundaries that regular editing operations, like backspacing or lifting, won't cross. An example of a node that should probably have this enabled is a table cell.

Maps to the isolating attribute in the ProseMirror schema.

const CustomNode = Node.create({
  name: 'customNode',

  isolating: true,
})

addNodeView (Advanced)

For advanced use cases, where you need to execute JavaScript inside your nodes, for example to render a sophisticated interface around an image, you need to learn about node views.

They are really powerful, but also complex. In a nutshell, you need to return a parent DOM element, and a DOM element where the content should be rendered in. Look at the following, simplified example:

import Image from '@tiptap/extension-image'

const CustomImage = Image.extend({
  addNodeView() {
    return () => {
      const container = document.createElement('div')

      container.addEventListener('click', (event) => {
        alert('clicked on the container')
      })

      const content = document.createElement('div')
      container.append(content)

      return {
        dom: container,
        contentDOM: content,
      }
    }
  },
})

There is a whole lot to learn about node views, so head over to the dedicated section in our guide about node views for more information. If you are looking for a real-world example, look at the source code of the TaskItem node. This is using a node view to render the checkboxes.