Skip to main content
Tiptap provides first-class support for React with hooks, components, and utilities that integrate seamlessly with React’s ecosystem.

Installation

1

Install the packages

Install the React package along with the core package and any extensions you need:
npm install @tiptap/react @tiptap/core @tiptap/starter-kit
2

Import and use

Import the necessary components and hooks in your React component:
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

Core Concepts

useEditor Hook

The useEditor hook is the primary way to create and manage an editor instance in React. It handles the editor lifecycle automatically.

Type Signature

function useEditor(
  options: UseEditorOptions,
  deps?: DependencyList
): Editor | null

interface UseEditorOptions extends Partial<EditorOptions> {
  /**
   * Whether to render the editor on the first render.
   * If client-side rendering, set this to `true`.
   * If server-side rendering, set this to `false`.
   * @default true
   */
  immediatelyRender?: boolean
  
  /**
   * Whether to re-render the editor on each transaction.
   * This is legacy behavior that will be removed in future versions.
   * @default false
   */
  shouldRerenderOnTransaction?: boolean
}

Basic Usage

import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World!</p>',
  })

  return <EditorContent editor={editor} />
}

SSR Configuration

For server-side rendering (Next.js, Remix, etc.), you must set immediatelyRender to false:
function MyEditor() {
  const editor = useEditor({
    immediatelyRender: false, // Required for SSR
    extensions: [StarterKit],
    content: '<p>Hello World!</p>',
  })

  return <EditorContent editor={editor} />
}

Performance Optimization

For better performance, disable transaction re-renders and use useEditorState instead:
import { useEditor, useEditorState, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function MyEditor() {
  const editor = useEditor({
    immediatelyRender: true,
    shouldRerenderOnTransaction: false, // Disable automatic re-renders
    extensions: [StarterKit],
    content: '<p>Hello World!</p>',
  })

  // Select only the state you need
  const { isBold, isItalic } = useEditorState({
    editor,
    selector: (ctx) => ({
      isBold: ctx.editor.isActive('bold'),
      isItalic: ctx.editor.isActive('italic'),
    }),
  })

  return (
    <div>
      <button onClick={() => editor?.chain().focus().toggleBold().run()}>
        Bold {isBold ? '(active)' : ''}
      </button>
      <button onClick={() => editor?.chain().focus().toggleItalic().run()}>
        Italic {isItalic ? '(active)' : ''}
      </button>
      <EditorContent editor={editor} />
    </div>
  )
}

EditorContent Component

The EditorContent component renders the actual editor interface.
import type { Editor } from '@tiptap/core'
import { EditorContent } from '@tiptap/react'

interface EditorContentProps {
  editor: Editor | null
  className?: string
  style?: React.CSSProperties
  // ... other HTML div props
}

Usage Example

function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World!</p>',
  })

  return (
    <EditorContent 
      editor={editor} 
      className="prose max-w-none"
      style={{ minHeight: '200px' }}
    />
  )
}

New Tiptap Component API

Tiptap 3.18+ introduces a new component-based API that provides a cleaner, more React-idiomatic way to build editors.

Tiptap Provider Component

The Tiptap component provides the editor instance via context, making it available to all child components.
import { Tiptap, useEditor, useTiptap } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function Toolbar() {
  // Access editor from context
  const { editor } = useTiptap()

  return (
    <div>
      <button onClick={() => editor.chain().focus().toggleBold().run()}>
        Bold
      </button>
      <button onClick={() => editor.chain().focus().toggleItalic().run()}>
        Italic
      </button>
    </div>
  )
}

function App() {
  const editor = useEditor({ 
    extensions: [StarterKit],
    content: '<p>Hello World!</p>'
  })

  return (
    <Tiptap editor={editor}>
      <Toolbar />
      <Tiptap.Content className="editor" />
    </Tiptap>
  )
}

useTiptap Hook

Access the editor instance from any component inside the Tiptap provider:
import { useTiptap } from '@tiptap/react'

function WordCount() {
  const { editor } = useTiptap()
  const count = editor.state.doc.textContent.split(/\s+/).filter(Boolean).length

  return <span>{count} words</span>
}

useTiptapState Hook

Select specific editor state slices for optimized re-renders:
import { useTiptapState } from '@tiptap/react'

function FormattingButtons() {
  const state = useTiptapState(
    (ctx) => ({
      isBold: ctx.editor.isActive('bold'),
      isItalic: ctx.editor.isActive('italic'),
      isStrike: ctx.editor.isActive('strike'),
    }),
    // Optional equality function
    (a, b) => a.isBold === b.isBold && a.isItalic === b.isItalic && a.isStrike === b.isStrike
  )

  return (
    <div>
      <button disabled={!state.isBold}>Bold</button>
      <button disabled={!state.isItalic}>Italic</button>
      <button disabled={!state.isStrike}>Strike</button>
    </div>
  )
}

Complete Working Example

import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import './styles.css'

export default function Editor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: `
      <h2>Hi there,</h2>
      <p>this is a <em>basic</em> example of <strong>Tiptap</strong>.</p>
    `,
  })

  return <EditorContent editor={editor} />
}

Event Handlers

Handle editor events through the useEditor options:
const editor = useEditor({
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
  onUpdate: ({ editor }) => {
    console.log('Content updated:', editor.getHTML())
  },
  onCreate: ({ editor }) => {
    console.log('Editor created')
  },
  onFocus: ({ editor, event }) => {
    console.log('Editor focused')
  },
  onBlur: ({ editor, event }) => {
    console.log('Editor blurred')
  },
  onSelectionUpdate: ({ editor }) => {
    console.log('Selection updated')
  },
})

Dependencies Array

The second parameter of useEditor accepts a dependencies array, similar to useEffect. When dependencies change, the editor is recreated:
function MyEditor({ userId }) {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World!</p>',
  }, [userId]) // Recreate editor when userId changes

  return <EditorContent editor={editor} />
}

TypeScript Support

Tiptap has full TypeScript support with comprehensive type definitions:
import type { Editor } from '@tiptap/core'
import type { UseEditorOptions } from '@tiptap/react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

const editorConfig: UseEditorOptions = {
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
}

function MyEditor() {
  const editor: Editor | null = useEditor(editorConfig)

  return <EditorContent editor={editor} />
}

Advanced: Custom Node Views

Create React components as custom node views:
import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap/react'
import { Node } from '@tiptap/core'

// React component for the node
function Component(props) {
  return (
    <NodeViewWrapper className="custom-node">
      <div>Custom node content:</div>
      <NodeViewContent />
    </NodeViewWrapper>
  )
}

// Define the extension
const CustomNode = Node.create({
  name: 'customNode',
  
  group: 'block',
  
  content: 'inline*',
  
  parseHTML() {
    return [{ tag: 'div[data-type="custom-node"]' }]
  },
  
  renderHTML({ HTMLAttributes }) {
    return ['div', { 'data-type': 'custom-node', ...HTMLAttributes }, 0]
  },
  
  addNodeView() {
    return ReactNodeViewRenderer(Component)
  },
})
Bubble and floating menus are now separate imports to keep @floating-ui/dom optional:
import { BubbleMenu, FloatingMenu } from '@tiptap/react/menus'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function Editor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World!</p>',
  })

  return (
    <>
      <EditorContent editor={editor} />
      
      <BubbleMenu editor={editor}>
        <button onClick={() => editor?.chain().focus().toggleBold().run()}>
          Bold
        </button>
      </BubbleMenu>
      
      <FloatingMenu editor={editor}>
        <button onClick={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()}>
          H1
        </button>
      </FloatingMenu>
    </>
  )
}

Next Steps

Extensions

Explore available extensions to enhance your editor

Commands

Learn about editor commands and chains

Styling

Customize the editor’s appearance

Node Views

Create interactive custom nodes