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

# Schema

> Learn how Tiptap generates a ProseMirror schema from extensions and how it defines your document structure.

The schema is the heart of your editor's document structure. It defines what nodes and marks are available, how they can be nested, and what attributes they can have.

## What is a Schema?

In ProseMirror (and Tiptap), a **schema** is a specification that defines:

<CardGroup cols={2}>
  <Card title="Node Types" icon="cube">
    What block and inline elements can exist in your document (paragraphs, headings, images, etc.)
  </Card>

  <Card title="Mark Types" icon="highlighter">
    What formatting can be applied to text (bold, italic, links, etc.)
  </Card>

  <Card title="Content Rules" icon="diagram-project">
    What content each node can contain (e.g., a paragraph can contain inline content)
  </Card>

  <Card title="Attributes" icon="sliders">
    What attributes nodes and marks can have (e.g., heading level, link href)
  </Card>
</CardGroup>

## How Tiptap Creates a Schema

Unlike ProseMirror where you define the schema manually, **Tiptap automatically generates the schema from your extensions**.

```typescript theme={null}
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'

const editor = new Editor({
  extensions: [
    StarterKit,
  ],
})

// Schema is automatically generated
console.log(editor.schema)
```

Source: `/home/daytona/workspace/source/packages/core/src/Editor.ts:469`

### Schema Generation Process

1. **Extensions are collected** - All extensions (nodes, marks, and generic extensions) are gathered
2. **Extensions are resolved** - Dependencies and priorities are resolved
3. **Schema is built** - Node specs and mark specs are extracted and combined
4. **ProseMirror schema is created** - A ProseMirror `Schema` object is instantiated

Source: `/home/daytona/workspace/source/packages/core/src/helpers/getSchema.ts:8`

## Accessing the Schema

You can access the schema through the editor:

```typescript theme={null}
// Get the schema
const schema = editor.schema

// Access node types
const paragraphType = schema.nodes.paragraph
const headingType = schema.nodes.heading
const imageType = schema.nodes.image

// Access mark types
const boldType = schema.marks.bold
const linkType = schema.marks.link
const italicType = schema.marks.italic
```

Source: `/home/daytona/workspace/source/packages/core/src/Editor.ts:65`

## Schema Structure

### Node Specs

Each node in the schema has a specification that defines its behavior:

```typescript theme={null}
{
  // The node type
  paragraph: {
    // Content expression
    content: 'inline*',
    
    // Group membership
    group: 'block',
    
    // Parsing rules
    parseDOM: [
      { tag: 'p' }
    ],
    
    // Rendering rules
    toDOM: () => ['p', 0],
    
    // Attributes
    attrs: {
      textAlign: {
        default: 'left'
      }
    }
  }
}
```

### Mark Specs

Each mark in the schema has a specification:

```typescript theme={null}
{
  // The mark type
  bold: {
    // Parsing rules
    parseDOM: [
      { tag: 'strong' },
      { tag: 'b' },
      { style: 'font-weight', getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null }
    ],
    
    // Rendering rules
    toDOM: () => ['strong', 0],
    
    // Whether the mark is inclusive (continues when typing at edge)
    inclusive: true,
    
    // Which marks this mark excludes
    excludes: undefined
  }
}
```

## Content Expressions

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

### Syntax

| Expression                | Meaning                                       |
| ------------------------- | --------------------------------------------- |
| `inline*`                 | Zero or more inline elements                  |
| `block+`                  | One or more block elements                    |
| `text*`                   | Zero or more text nodes                       |
| `paragraph+`              | One or more paragraphs                        |
| `heading paragraph*`      | A heading followed by zero or more paragraphs |
| `(paragraph \| heading)+` | One or more paragraphs or headings            |

### Examples

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

// Paragraph: can contain any inline content
export const Paragraph = Node.create({
  name: 'paragraph',
  group: 'block',
  content: 'inline*', // Zero or more inline elements
})

// Heading: can contain inline content
export const Heading = Node.create({
  name: 'heading',
  group: 'block',
  content: 'inline*',
})

// BlockQuote: must contain at least one block
export const Blockquote = Node.create({
  name: 'blockquote',
  group: 'block',
  content: 'block+', // One or more block elements
})

// ListItem: specific content structure
export const ListItem = Node.create({
  name: 'listItem',
  content: 'paragraph block*', // Paragraph followed by zero or more blocks
})

// Image: no content (leaf node)
export const Image = Node.create({
  name: 'image',
  group: 'block',
  atom: true,
  // No content expression = cannot contain anything
})

// Doc: top-level node
export const Doc = Node.create({
  name: 'doc',
  topNode: true,
  content: 'block+', // Must contain at least one block
})
```

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

## Groups

Groups let you reference multiple node types in content expressions:

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

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

export const Heading = Node.create({
  name: 'heading',
  group: 'block', // Also belongs to 'block' group
  content: 'inline*',
})

export const Image = Node.create({
  name: 'image',
  group: 'block',
  atom: true,
})

// Use the group in content expressions
export const Doc = Node.create({
  name: 'doc',
  topNode: true,
  content: 'block+', // Accepts any node in the 'block' group
})

export const Blockquote = Node.create({
  name: 'blockquote',
  group: 'block',
  content: 'block+', // Can contain paragraphs, headings, images, etc.
})
```

### Common Groups

* **`block`** - Block-level content (paragraphs, headings, etc.)
* **`inline`** - Inline content (text, hard breaks, inline images, etc.)
* **`list`** - List-related nodes (bullet lists, ordered lists, etc.)

You can also create custom groups:

```typescript theme={null}
export const CalloutBlock = Node.create({
  name: 'calloutBlock',
  group: 'block callout', // Multiple groups
  content: 'block+',
})
```

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

## Document Structure

Every Tiptap document has a root `doc` node that contains all other content:

```json theme={null}
{
  "type": "doc",
  "content": [
    {
      "type": "heading",
      "attrs": { "level": 1 },
      "content": [
        { "type": "text", "text": "Hello World" }
      ]
    },
    {
      "type": "paragraph",
      "content": [
        { "type": "text", "text": "This is a " },
        {
          "type": "text",
          "marks": [{ "type": "bold" }],
          "text": "bold"
        },
        { "type": "text", "text": " word." }
      ]
    },
    {
      "type": "image",
      "attrs": {
        "src": "https://example.com/image.jpg",
        "alt": "Example"
      }
    }
  ]
}
```

## Schema Validation

The schema enforces content rules. Invalid content is rejected:

```typescript theme={null}
// Valid: heading can contain inline content
editor.commands.insertContent({
  type: 'heading',
  attrs: { level: 1 },
  content: [{ type: 'text', text: 'Title' }]
})

// Invalid: heading cannot contain a paragraph (block content)
editor.commands.insertContent({
  type: 'heading',
  attrs: { level: 1 },
  content: [
    { type: 'paragraph', content: [{ type: 'text', text: 'Invalid' }] }
  ]
}) // This will be rejected!
```

## Custom Schema Example

Here's how to create a custom document structure:

```typescript theme={null}
import { Editor } from '@tiptap/core'
import { Document } from '@tiptap/extension-document'
import { Text } from '@tiptap/extension-text'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Heading } from '@tiptap/extension-heading'
import { Bold } from '@tiptap/extension-bold'
import { Italic } from '@tiptap/extension-italic'

// Create a custom doc node that requires a title
const CustomDoc = Document.extend({
  content: 'heading paragraph+',
})

const editor = new Editor({
  extensions: [
    CustomDoc,
    Text,
    Paragraph,
    Heading.configure({ levels: [1] }), // Only h1
    Bold,
    Italic,
  ],
  content: {
    type: 'doc',
    content: [
      {
        type: 'heading',
        attrs: { level: 1 },
        content: [{ type: 'text', text: 'Title' }]
      },
      {
        type: 'paragraph',
        content: [{ type: 'text', text: 'Content' }]
      }
    ]
  },
})

// This document structure is now enforced:
// - Must start with a heading (level 1)
// - Must have at least one paragraph
// - Headings can only be level 1
```

## Inspecting the Schema

### View Node Types

```typescript theme={null}
// Get all node type names
const nodeNames = Object.keys(editor.schema.nodes)
console.log(nodeNames)
// ['doc', 'paragraph', 'text', 'heading', 'blockquote', ...]

// Get node type details
const paragraphType = editor.schema.nodes.paragraph
console.log(paragraphType.spec.content) // 'inline*'
console.log(paragraphType.spec.group) // 'block'
```

### View Mark Types

```typescript theme={null}
// Get all mark type names
const markNames = Object.keys(editor.schema.marks)
console.log(markNames)
// ['bold', 'italic', 'link', 'code', ...]

// Get mark type details
const boldType = editor.schema.marks.bold
console.log(boldType.spec.inclusive) // true
```

### Check Content Validity

```typescript theme={null}
import { Fragment, Slice } from '@tiptap/pm/model'

// Check if content is valid for a node type
const paragraphType = editor.schema.nodes.paragraph
const content = Fragment.from([
  editor.schema.text('Hello '),
  editor.schema.text('world', [editor.schema.marks.bold.create()])
])

const isValid = paragraphType.validContent(content)
console.log(isValid) // true
```

## Using Schema Types in Commands

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

// Use in commands
editor.commands.setNode(headingType, { level: 1 })

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

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

## Advanced: Schema Introspection

```typescript theme={null}
// Iterate over all node types
for (const [name, nodeType] of Object.entries(editor.schema.nodes)) {
  console.log(`Node: ${name}`)
  console.log(`  Content: ${nodeType.spec.content}`)
  console.log(`  Group: ${nodeType.spec.group}`)
  console.log(`  Inline: ${nodeType.isInline}`)
  console.log(`  Block: ${nodeType.isBlock}`)
  console.log(`  Leaf: ${nodeType.isLeaf}`)
}

// Iterate over all mark types
for (const [name, markType] of Object.entries(editor.schema.marks)) {
  console.log(`Mark: ${name}`)
  console.log(`  Inclusive: ${markType.spec.inclusive}`)
  console.log(`  Excludes: ${markType.spec.excludes}`)
}
```

## Best Practices

<Card title="Start with StarterKit" icon="rocket">
  Use StarterKit as a foundation, then add or remove extensions as needed.

  ```typescript theme={null}
  import StarterKit from '@tiptap/starter-kit'

  const editor = new Editor({
    extensions: [
      StarterKit.configure({
        heading: {
          levels: [1, 2, 3],
        },
      }),
    ],
  })
  ```
</Card>

<Card title="Be Explicit with Content" icon="diagram-project">
  Clearly define what content each node can contain to prevent invalid structures.

  ```typescript theme={null}
  // Good: Explicit content rules
  content: 'heading paragraph+'

  // Less good: Too permissive
  content: 'block*'
  ```
</Card>

<Card title="Use Groups Wisely" icon="layer-group">
  Group similar nodes together to simplify content expressions.

  ```typescript theme={null}
  // Create a custom group
  group: 'block customBlock'

  // Reference it in content
  content: 'customBlock+'
  ```
</Card>

<Card title="Validate Content" icon="shield-check">
  Enable content validation in development to catch schema violations early.

  ```typescript theme={null}
  const editor = new Editor({
    enableContentCheck: true,
    onContentError: ({ error }) => {
      console.error('Invalid content:', error)
    },
  })
  ```
</Card>

## Schema vs Extensions

| Aspect         | ProseMirror Schema    | Tiptap Extensions             |
| -------------- | --------------------- | ----------------------------- |
| Definition     | Manual JSON object    | Declarative extension classes |
| Flexibility    | Full control, verbose | High-level API, concise       |
| Maintenance    | Manual updates        | Automatic from extensions     |
| Composability  | Difficult             | Easy to mix and match         |
| Learning Curve | Steep                 | Gentle                        |

<Info>
  Tiptap's extension system is built on top of ProseMirror's schema. You get the power of ProseMirror with a much simpler API.
</Info>

## Related

<CardGroup cols={2}>
  <Card title="Nodes & Marks" icon="cube" href="/core-concepts/nodes-and-marks">
    Learn how to create nodes and marks
  </Card>

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

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

  <Card title="Commands" icon="terminal" href="/core-concepts/commands">
    Learn how to manipulate content
  </Card>
</CardGroup>
