---
title: "Node views with React"
description: "Use React to build custom node views in Tiptap. Direct manipulation of node properties and interactive content."
canonical_url: "https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views/react"
---

# Node views with React

Use React to build custom node views in Tiptap. Direct manipulation of node properties and interactive content.

Using Vanilla JavaScript can feel complex if you are used to work in React. Good news: You can use regular React components in your node views, too. There is just a little bit you need to know, but let’s go through this one by one.

## Render a React component

Here is what you need to do to render React components inside your editor:

1. [Create a node extension](https://tiptap.dev/docs/editor/extensions/custom-extensions.md)
2. Create a React component
3. Pass that component to the provided `ReactNodeViewRenderer`
4. Register it with `addNodeView()`
5. [Configure Tiptap to use your new node extension](https://tiptap.dev/docs/editor/getting-started/configure.md)

This is how your node extension could look like:

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

export default Node.create({
  // configuration …

  addNodeView() {
    return ReactNodeViewRenderer(Component)
  },
})
```

There is a little bit of magic required to make this work. But don’t worry, we provide a wrapper component you can use to get started easily. Don’t forget to add it to your custom React component, like shown below:

```jsx
<NodeViewWrapper className="react-component">React Component</NodeViewWrapper>
```

Got it? Let’s see it in action. Feel free to copy the below example to get started.

> **Interactive demo:** [ReactComponent](https://embed.tiptap.dev/preview/GuideNodeViews/ReactComponent?inline=false\&hideSource=false)

That component doesn’t interact with the editor, though. Time to wire it up.

## Access node attributes

The `ReactNodeViewRenderer` which you use in your node extension, passes a few very helpful props to your custom React component. One of them is the `node` prop. Let’s say you have [added an attribute](https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing.md#attributes) named `count` to your node extension (like we did in the above example) you could access it like this:

```js
props.node.attrs.count
```

## Update node attributes

You can even update node attributes from your node, with the help of the `updateAttributes` prop passed to your component. Pass an object with updated attributes to the `updateAttributes` prop:

```js
export default (props) => {
  const increase = () => {
    props.updateAttributes({
      count: props.node.attrs.count + 1,
    })
  }

  // …
}
```

And yes, all of that is reactive, too. A pretty seamless communication, isn’t it?

## Adding a content editable

There is another component called `NodeViewContent` which helps you adding editable content to your node view. Here is an example:

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

export default () => {
  return (
    <NodeViewWrapper className="react-component">
      <span className="label" contentEditable={false}>
        React Component
      </span>

      <NodeViewContent className="content" />
    </NodeViewWrapper>
  )
}
```

You don’t need to add those `className` attributes, feel free to remove them or pass other class names. Try it out in the following example:

> **Interactive demo:** [ReactComponentContent](https://embed.tiptap.dev/preview/GuideNodeViews/ReactComponentContent?inline=false\&hideSource=false)

Keep in mind that this content is rendered by Tiptap. That means you need to tell what kind of content is allowed, for example with `content: 'inline*'` in your node extension (that’s what we use in the above example).

The `NodeViewWrapper` and `NodeViewContent` components render a `<div>` HTML tag (`<span>` for inline nodes), but you can change that. For example `<NodeViewContent as="p">` should render a paragraph. One limitation though: That tag must not change during runtime.

## Changing the default content tag for a node view

By default a node view rendered by `ReactNodeViewRenderer` will always have a wrapping `div` inside. If you want to change the type of this node, you can the `contentDOMElementTag` to the `ReactNodeViewRenderer` options:

```js
// this will turn the div into a header tag
return ReactNodeViewRenderer(Component, { contentDOMElementTag: 'header' })
```

## Changing the wrapping DOM element

To change the wrapping DOM elements tag, you can use the `as` option on the `ReactNodeViewRenderer` function to change the default tag name.

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

export default Node.create({
  // configuration …

  addNodeView() {
    return ReactNodeViewRenderer(Component, { as: 'main' })
  },
})
```

## Updating `selected` on a text selection

By default, the `selected` prop only becomes `true` when the node itself is selected with a `NodeSelection`. That works great for atom nodes, but it does not fire when the cursor is simply placed inside a node that has text content. If you want `selected` to also reflect that case, enable the `selectedOnTextSelection` option on the `ReactNodeViewRenderer`:

```js
return ReactNodeViewRenderer(Component, { selectedOnTextSelection: true })
```

With this option turned on, `selected` will also be `true` whenever a `TextSelection` is fully contained within the node’s range — for example, when the cursor is placed somewhere inside the node’s content. Selections that only partially overlap the node are not considered selected.

## Tracking node position

Node views do not re-render when decorations or position change without content changes. ProseMirror handles decorations natively on the contentDOM, and the `getPos()` closure always returns the current position when called — even without a re-render.

If your component needs `getPos()` to stay reactive in render output (e.g. a drag handle that displays `pos: 42`), enable `trackNodeViewPosition`:

```js
return ReactNodeViewRenderer(Component, { trackNodeViewPosition: true })
```

When enabled, the component re-renders on every position shift. Use with caution — frequent re-renders can impact performance.

## All available props

Here is the full list of what props you can expect:

| Prop               | Description                                                                                                                                                                                                  |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| editor             | The editor instance                                                                                                                                                                                          |
| node               | The current node                                                                                                                                                                                             |
| decorations        | An array of decorations                                                                                                                                                                                      |
| selected           | `true` when there is a `NodeSelection` at the current node view. Also `true` on a `TextSelection` fully inside the node when [`selectedOnTextSelection`](#updating-selected-on-a-text-selection) is enabled. |
| extension          | Access to the node extension, for example to get options                                                                                                                                                     |
| getPos()           | Get the document position of the current node                                                                                                                                                                |
| updateAttributes() | Update attributes of the current node                                                                                                                                                                        |
| deleteNode()       | Delete the current node                                                                                                                                                                                      |

## Dragging

To make your node views draggable, set `draggable: true` in the extension and add `data-drag-handle` to the DOM element that should function as the drag handle.
