---
title: "Nested node views"
description: "Learn how to build parent and child node views with nested NodeViewContent in Vue and React."
canonical_url: "https://tiptap.dev/docs/guides/nested-node-view-content"
---

# Nested node views

Learn how to build parent and child node views with nested NodeViewContent in Vue and React.

This guide shows how to create a parent node that contains multiple child nodes, each rendered with its own component. This pattern is useful when you need structured content with distinct visual representations inside a single block.

If you are new to node views, start with the [Node views overview](https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views.md) and the framework-specific guides for [React](https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views/react.md) and [Vue](https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views/vue.md).

## The key idea

The most important part of this pattern is defining your schema correctly. The parent node must explicitly declare which child nodes are allowed, and each child node needs its own node view with a `NodeViewContent` component.

In this example, a `multiNode` parent contains two child nodes: `firstNode` and `secondNode`. Each node is rendered by its own component.

> **Schema first:**
>
> If nested `NodeViewContent` does not render as expected, double-check the `content` expression on
> your parent and child node extensions before debugging your components.

## Inserting the parent node

When inserting the parent node, create the full structure in one step so the document always contains the expected children:

```js
editor
  .chain()
  .focus()
  .insertContent(
    editor.schema.node('multiNode', null, [
      editor.schema.node('firstNode', null, [editor.schema.node('paragraph')]),
      editor.schema.node('secondNode', null, [editor.schema.node('paragraph')]),
    ]),
  )
  .run()
```

## Vue

### Parent node extension

```js
import MultiNode from './MultiNode.vue'
import { Node, mergeAttributes } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-3'

export default Node.create({
  name: 'multiNode',
  group: 'block',
  content: 'firstNode secondNode',
  parseHTML() {
    return [{ tag: 'multi-node' }]
  },
  renderHTML({ HTMLAttributes }) {
    return ['multi-node', mergeAttributes(HTMLAttributes), 0]
  },
  addNodeView() {
    return VueNodeViewRenderer(MultiNode)
  },
})
```

### Parent node view

The parent component provides the wrapper structure. Use `<node-view-content />` to render the child node views inside it.

```html
<template>
  <node-view-wrapper class="multi-node">
    <node-view-content />
  </node-view-wrapper>
</template>

<script setup>
  import { NodeViewContent, NodeViewWrapper } from '@tiptap/vue-3'
</script>
```

### Child node extension

Child nodes keep the same group as the parent and define their own editable content.

```js
import FirstNode from './FirstNode.vue'
import { Node, mergeAttributes } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-3'

export default Node.create({
  name: 'firstNode',
  group: 'block',
  content: 'block+',
  parseHTML() {
    return [{ tag: 'first-node' }]
  },
  renderHTML({ HTMLAttributes }) {
    return ['first-node', mergeAttributes(HTMLAttributes), 0]
  },
  addNodeView() {
    return VueNodeViewRenderer(FirstNode)
  },
})
```

### Child node view

```html
<template>
  <node-view-wrapper class="first-node">
    <node-view-content />
  </node-view-wrapper>
</template>

<script setup>
  import { NodeViewContent, NodeViewWrapper } from '@tiptap/vue-3'
</script>
```

The `secondNode` extension and view follow the same pattern as `firstNode`.

## React

The React implementation follows the same schema rules. The main difference is that you use `ReactNodeViewRenderer`, `NodeViewWrapper`, and `NodeViewContent` from `@tiptap/react`.

### Parent node extension

```js
import { Node, mergeAttributes } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import MultiNode from './MultiNode.jsx'

export default Node.create({
  name: 'multiNode',
  group: 'block',
  content: 'firstNode secondNode',
  parseHTML() {
    return [{ tag: 'multi-node' }]
  },
  renderHTML({ HTMLAttributes }) {
    return ['multi-node', mergeAttributes(HTMLAttributes), 0]
  },
  addNodeView() {
    return ReactNodeViewRenderer(MultiNode)
  },
})
```

### Parent node view

```jsx
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'

export default function MultiNode() {
  return (
    <NodeViewWrapper className="multi-node">
      <NodeViewContent />
    </NodeViewWrapper>
  )
}
```

### Child node extension

```js
import { Node, mergeAttributes } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import FirstNode from './FirstNode.jsx'

export default Node.create({
  name: 'firstNode',
  group: 'block',
  content: 'block+',
  parseHTML() {
    return [{ tag: 'first-node' }]
  },
  renderHTML({ HTMLAttributes }) {
    return ['first-node', mergeAttributes(HTMLAttributes), 0]
  },
  addNodeView() {
    return ReactNodeViewRenderer(FirstNode)
  },
})
```

### Child node view

```jsx
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'

export default function FirstNode() {
  return (
    <NodeViewWrapper className="first-node">
      <NodeViewContent />
    </NodeViewWrapper>
  )
}
```

The `secondNode` extension and view follow the same pattern as `firstNode`.

## Related resources

- [Node views overview](https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views.md)
- [Node views with React](https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views/react.md)
- [Node views with Vue](https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views/vue.md)
- [Create a custom node](https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new/node.md)
