> ## 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.

# Nodes & Marks

> Learn the difference between nodes and marks, and how they form the structure of your Tiptap document.

In Tiptap (and ProseMirror), your document is made up of **nodes** and **marks**. Understanding the difference is crucial for working effectively with the editor.

## Nodes vs Marks

<CardGroup cols={2}>
  <Card title="Nodes" icon="cube">
    Nodes are **structural** pieces of your document. They define the content type and can contain other nodes or text.

    **Examples:** Paragraph, Heading, Image, CodeBlock, BulletList
  </Card>

  <Card title="Marks" icon="highlighter">
    Marks are **formatting** applied to text. They annotate nodes without changing the structure.

    **Examples:** Bold, Italic, Link, Code, Highlight
  </Card>
</CardGroup>

### Key Differences

| Aspect      | Nodes                    | Marks                     |
| ----------- | ------------------------ | ------------------------- |
| Purpose     | Define structure         | Define formatting         |
| Can contain | Other nodes, text, marks | Nothing (they wrap text)  |
| Examples    | `<p>`, `<h1>`, `<img>`   | `<strong>`, `<em>`, `<a>` |
| Can overlap | No                       | Yes                       |
| Can nest    | Yes                      | No                        |

<Info>
  **Rule of thumb:** If it's a block element or defines structure, it's a node. If it's formatting that can be applied to text, it's a mark.
</Info>

## Creating Nodes

Nodes are created using `Node.create()`:

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

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

export const Paragraph = Node.create<ParagraphOptions>({
  name: 'paragraph',
  
  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)
      },
    }
  },
})
```

Source: `/home/daytona/workspace/source/packages/extension-paragraph/src/paragraph.ts:41`

### Node Configuration

<ResponseField name="name" type="string" required>
  The unique name of the node.

  ```typescript theme={null}
  name: 'paragraph'
  ```
</ResponseField>

<ResponseField name="group" type="string">
  The group(s) this node belongs to. Used in content expressions.

  ```typescript theme={null}
  group: 'block'
  // or multiple groups
  group: 'block list'
  ```

  Common groups:

  * `'block'` - Block-level content
  * `'inline'` - Inline content
  * `'list'` - List items

  Source: `/home/daytona/workspace/source/packages/core/src/Node.ts:83`
</ResponseField>

<ResponseField name="content" type="string">
  What content this node can contain. Uses ProseMirror's content expression syntax.

  ```typescript theme={null}
  // Can contain any number of inline elements
  content: 'inline*'

  // Must contain at least one block
  content: 'block+'

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

  // No content allowed (leaf node)
  content: undefined
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Node.ts:42`
</ResponseField>

<ResponseField name="inline" type="boolean">
  Whether this is an inline node.

  ```typescript theme={null}
  inline: true
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Node.ts:96`
</ResponseField>

<ResponseField name="atom" type="boolean">
  If true, the node is treated as a single unit (cursor cannot enter it).

  ```typescript theme={null}
  atom: true // e.g., for images, videos
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Node.ts:113`
</ResponseField>

<ResponseField name="selectable" type="boolean" default="true">
  Whether the node can be selected.

  ```typescript theme={null}
  selectable: false
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Node.ts:131`
</ResponseField>

<ResponseField name="draggable" type="boolean" default="false">
  Whether the node can be dragged.

  ```typescript theme={null}
  draggable: true
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Node.ts:147`
</ResponseField>

<ResponseField name="isolating" type="boolean" default="false">
  If true, boundaries of this node are treated as boundaries for editing operations.

  ```typescript theme={null}
  isolating: true // e.g., for table cells
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Node.ts:236`
</ResponseField>

<ResponseField name="parseHTML" type="() => ParseRule[]">
  Rules for parsing HTML into this node.

  ```typescript theme={null}
  parseHTML() {
    return [
      { tag: 'p' },
      { tag: 'div', attrs: { 'data-type': 'paragraph' } },
    ]
  }
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Node.ts:256`
</ResponseField>

<ResponseField name="renderHTML" type="({ node, HTMLAttributes }) => DOMOutputSpec">
  How to render this node to HTML.

  ```typescript theme={null}
  renderHTML({ node, HTMLAttributes }) {
    return ['p', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
  }
  ```

  The `0` indicates where child content should be inserted.

  Source: `/home/daytona/workspace/source/packages/core/src/Node.ts:284`
</ResponseField>

<ResponseField name="addAttributes" type="() => Attributes">
  Define custom attributes for the node.

  ```typescript theme={null}
  addAttributes() {
    return {
      level: {
        default: 1,
        rendered: false,
      },
      textAlign: {
        default: 'left',
        renderHTML: attributes => ({
          style: `text-align: ${attributes.textAlign}`,
        }),
        parseHTML: element => element.style.textAlign || 'left',
      },
    }
  }
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Node.ts:326`
</ResponseField>

## Creating Marks

Marks are created using `Mark.create()`:

```typescript theme={null}
import { Mark, mergeAttributes } from '@tiptap/core'

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

export const Bold = Mark.create<BoldOptions>({
  name: 'bold',
  
  parseHTML() {
    return [
      { tag: 'strong' },
      { tag: 'b' },
      { 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 {
      toggleBold: () => ({ commands }) => {
        return commands.toggleMark(this.name)
      },
    }
  },
})
```

Source: `/home/daytona/workspace/source/packages/extension-bold/src/bold.tsx:56`

### Mark Configuration

<ResponseField name="name" type="string" required>
  The unique name of the mark.

  ```typescript theme={null}
  name: 'bold'
  ```
</ResponseField>

<ResponseField name="inclusive" type="boolean | ((config) => boolean)">
  Whether the mark should be included when typing at the edge.

  ```typescript theme={null}
  inclusive: true
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Mark.ts:32`
</ResponseField>

<ResponseField name="excludes" type="string">
  Which other marks this mark excludes. Use `'_'` to exclude all marks.

  ```typescript theme={null}
  // Exclude specific marks
  excludes: 'bold italic'

  // Exclude all other marks (for code)
  excludes: '_'
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Mark.ts:45`
</ResponseField>

<ResponseField name="exitable" type="boolean">
  Whether the user can exit the mark by pressing the right arrow at the end.

  ```typescript theme={null}
  exitable: true // e.g., for links
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Mark.ts:58`
</ResponseField>

<ResponseField name="keepOnSplit" type="boolean">
  Whether the mark should persist when a node is split.

  ```typescript theme={null}
  keepOnSplit: false // e.g., for links
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Mark.ts:27`
</ResponseField>

<ResponseField name="parseHTML" type="() => ParseRule[]">
  Rules for parsing HTML into this mark.

  ```typescript theme={null}
  parseHTML() {
    return [
      { tag: 'strong' },
      { tag: 'b' },
      { style: 'font-weight=bold' },
    ]
  }
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Mark.ts:102`
</ResponseField>

<ResponseField name="renderHTML" type="({ mark, HTMLAttributes }) => DOMOutputSpec">
  How to render this mark to HTML.

  ```typescript theme={null}
  renderHTML({ mark, HTMLAttributes }) {
    return ['strong', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
  }
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Mark.ts:113`
</ResponseField>

<ResponseField name="addAttributes" type="() => Attributes">
  Define custom attributes for the mark.

  ```typescript theme={null}
  addAttributes() {
    return {
      href: {
        default: null,
        parseHTML: element => element.getAttribute('href'),
        renderHTML: attributes => {
          if (!attributes.href) {
            return {}
          }
          return { href: attributes.href }
        },
      },
    }
  }
  ```

  Source: `/home/daytona/workspace/source/packages/core/src/Mark.ts:132`
</ResponseField>

## Content Expressions

Content expressions define what a node can contain. They use a simple syntax:

```typescript theme={null}
// Zero or more inline elements
content: 'inline*'

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

// Exactly one heading followed by zero or more blocks
content: 'heading block*'

// Either a paragraph or a blockquote
content: '(paragraph | blockquote)'

// A heading followed by a paragraph, then any number of blocks
content: 'heading paragraph block*'

// No content (leaf node)
content: undefined
```

### Common Patterns

| Pattern      | Meaning                      |                                   |
| ------------ | ---------------------------- | --------------------------------- |
| `inline*`    | Zero or more inline elements |                                   |
| `block+`     | One or more block elements   |                                   |
| `text*`      | Zero or more text nodes      |                                   |
| `paragraph+` | One or more paragraphs       |                                   |
| \`(block     | paragraph)\*\`               | Zero or more blocks or paragraphs |

## Node Examples

### Block Node: Heading

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

export const Heading = Node.create({
  name: 'heading',
  
  group: 'block',
  
  content: 'inline*',
  
  defining: true,
  
  addOptions() {
    return {
      levels: [1, 2, 3, 4, 5, 6],
      HTMLAttributes: {},
    }
  },
  
  addAttributes() {
    return {
      level: {
        default: 1,
        rendered: false,
      },
    }
  },
  
  parseHTML() {
    return this.options.levels.map(level => ({
      tag: `h${level}`,
      attrs: { level },
    }))
  },
  
  renderHTML({ node, HTMLAttributes }) {
    const level = this.options.levels.includes(node.attrs.level)
      ? node.attrs.level
      : this.options.levels[0]
    
    return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
  },
  
  addCommands() {
    return {
      setHeading: attributes => ({ commands }) => {
        return commands.setNode(this.name, attributes)
      },
      toggleHeading: attributes => ({ commands }) => {
        return commands.toggleNode(this.name, 'paragraph', attributes)
      },
    }
  },
})
```

### Inline Node: Mention

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

export const Mention = Node.create({
  name: 'mention',
  
  group: 'inline',
  
  inline: true,
  
  atom: true,
  
  addAttributes() {
    return {
      id: {
        default: null,
        parseHTML: element => element.getAttribute('data-id'),
        renderHTML: attributes => {
          if (!attributes.id) {
            return {}
          }
          return { 'data-id': attributes.id }
        },
      },
      label: {
        default: null,
        parseHTML: element => element.getAttribute('data-label'),
        renderHTML: attributes => {
          if (!attributes.label) {
            return {}
          }
          return { 'data-label': attributes.label }
        },
      },
    }
  },
  
  parseHTML() {
    return [{ tag: 'span[data-mention]' }]
  },
  
  renderHTML({ node, HTMLAttributes }) {
    return [
      'span',
      mergeAttributes(
        { 'data-mention': '' },
        this.options.HTMLAttributes,
        HTMLAttributes
      ),
      `@${node.attrs.label ?? node.attrs.id}`,
    ]
  },
})
```

### Leaf Node: Image

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

export const Image = Node.create({
  name: 'image',
  
  group: 'block',
  
  inline: false,
  
  atom: true,
  
  draggable: true,
  
  addAttributes() {
    return {
      src: {
        default: null,
      },
      alt: {
        default: null,
      },
      title: {
        default: null,
      },
    }
  },
  
  parseHTML() {
    return [{ tag: 'img' }]
  },
  
  renderHTML({ HTMLAttributes }) {
    return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
  },
  
  addCommands() {
    return {
      setImage: options => ({ commands }) => {
        return commands.insertContent({
          type: this.name,
          attrs: options,
        })
      },
    }
  },
})
```

## Mark Examples

### Simple Mark: Italic

```typescript theme={null}
import { Mark, mergeAttributes } from '@tiptap/core'

export const Italic = Mark.create({
  name: 'italic',
  
  parseHTML() {
    return [
      { tag: 'em' },
      { tag: 'i' },
      { style: 'font-style=italic' },
    ]
  },
  
  renderHTML({ HTMLAttributes }) {
    return ['em', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
  },
  
  addCommands() {
    return {
      toggleItalic: () => ({ commands }) => {
        return commands.toggleMark(this.name)
      },
    }
  },
  
  addKeyboardShortcuts() {
    return {
      'Mod-i': () => this.editor.commands.toggleItalic(),
      'Mod-I': () => this.editor.commands.toggleItalic(),
    }
  },
})
```

### Mark with Attributes: Link

```typescript theme={null}
import { Mark, mergeAttributes } from '@tiptap/core'

export const Link = Mark.create({
  name: 'link',
  
  inclusive: false,
  
  addOptions() {
    return {
      openOnClick: true,
      HTMLAttributes: {
        target: '_blank',
        rel: 'noopener noreferrer nofollow',
      },
    }
  },
  
  addAttributes() {
    return {
      href: {
        default: null,
      },
      target: {
        default: this.options.HTMLAttributes.target,
      },
    }
  },
  
  parseHTML() {
    return [{ tag: 'a[href]' }]
  },
  
  renderHTML({ HTMLAttributes }) {
    return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
  },
  
  addCommands() {
    return {
      setLink: attributes => ({ commands }) => {
        return commands.setMark(this.name, attributes)
      },
      toggleLink: attributes => ({ commands }) => {
        return commands.toggleMark(this.name, attributes)
      },
      unsetLink: () => ({ commands }) => {
        return commands.unsetMark(this.name)
      },
    }
  },
})
```

## Accessing Node/Mark Types

You can access the ProseMirror types from the schema:

```typescript theme={null}
// Get node type
const paragraphType = editor.schema.nodes.paragraph
const headingType = editor.schema.nodes.heading

// Get mark type
const boldType = editor.schema.marks.bold
const linkType = editor.schema.marks.link

// Use in commands
editor.commands.setNode(paragraphType)
editor.commands.toggleMark(boldType)
```

## Best Practices

<Card title="Choose the Right Type" icon="compass">
  * Use **nodes** for structural content (paragraphs, headings, images)
  * Use **marks** for text formatting (bold, italic, links)
  * If it can overlap with other formatting, it should be a mark
  * If it defines a block or structure, it should be a node
</Card>

<Card title="Define Content Carefully" icon="diagram-project">
  Think carefully about what content a node should accept:

  * Most block nodes use `content: 'inline*'`
  * Container nodes use `content: 'block+'`
  * Leaf nodes (images, etc.) have no content expression
</Card>

<Card title="Use Groups" icon="layer-group">
  Group similar nodes together:

  ```typescript theme={null}
  group: 'block list'
  ```

  This makes it easier to reference them in content expressions.
</Card>

<Card title="Parse and Render Correctly" icon="code">
  Ensure your `parseHTML` and `renderHTML` methods handle all variations:

  * Different HTML tags (`<b>` vs `<strong>`)
  * Inline styles
  * Data attributes
</Card>

## Related

<CardGroup cols={2}>
  <Card title="Extensions" icon="puzzle-piece" href="/core-concepts/extensions">
    Learn about the extension system
  </Card>

  <Card title="Schema" icon="diagram-project" href="/core-concepts/schema">
    Learn how nodes and marks create the schema
  </Card>

  <Card title="Commands" icon="terminal" href="/core-concepts/commands">
    Learn how to manipulate nodes and marks
  </Card>

  <Card title="Editor" icon="pen-to-square" href="/core-concepts/editor">
    Learn about the Editor class
  </Card>
</CardGroup>
