React

This guide describes how to integrate Tiptap with your React project. We're using Vite, but the workflow should be similar with other setups.

Create a React project (optional)

Start with a fresh React project called my-tiptap-project. Vite will set up everything we need.

# create a project with npm
npm create vite@latest my-tiptap-project -- --template react-ts

# OR, create a project with pnpm
pnpm create vite@latest my-tiptap-project --template react-ts

# OR, create a project with yarn
yarn create vite my-tiptap-project --template react-ts

# change directory
cd my-tiptap-project

Install Tiptap dependencies

Next, install the @tiptap/react package, @tiptap/pm (the ProseMirror library), and @tiptap/starter-kit, which includes the most common extensions to get started quickly.

  • @tiptap/react: The React bindings for Tiptap including Tiptap's core functionality.
  • @tiptap/pm: Tiptap's ProseMirror dependencies, which are required for the editor to function.
  • @tiptap/starter-kit: A collection of commonly used extensions that provide basic functionality like paragraphs, headings, bold, italic, and more.
npm install @tiptap/react @tiptap/pm @tiptap/starter-kit

If you followed steps 1 and 2, you can now start your project with npm run dev and open http://localhost:3000 in your browser.

Integrate Tiptap into your React app

Tiptap provides a declarative <Tiptap> component that simplifies editor setup and provides context to all child components. This is the recommended approach for integrating Tiptap with React.

Using the Tiptap component

Create a new component called Editor and add the following code in src/Editor.tsx:

// src/Editor.tsx
import { Tiptap, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

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

  return (
    <Tiptap instance={editor}>
      <Tiptap.Loading>Loading editor...</Tiptap.Loading>
      <MenuBar />
      <Tiptap.Content />
      <Tiptap.BubbleMenu>
        <button>Bold</button>
        <button>Italic</button>
      </Tiptap.BubbleMenu>
      <Tiptap.FloatingMenu>
        <button>Add heading</button>
      </Tiptap.FloatingMenu>
    </Tiptap>
  )
}

export default Editor

The <Tiptap> component provides several subcomponents:

ComponentDescription
Tiptap.ContentRenders the editor content area. Replaces <EditorContent editor={editor} />.
Tiptap.LoadingRenders its children only while the editor is initializing.
Tiptap.BubbleMenuA context-aware bubble menu that appears on text selection.
Tiptap.FloatingMenuA context-aware floating menu that appears on empty lines.

Add it to your app

Replace the content of src/App.tsx with your new Editor component:

import Editor from './Editor'

function App() {
  return (
    <div className="card">
      <Editor />
    </div>
  )
}

export default App

Accessing the editor in child components

The <Tiptap> component provides context that allows any child component to access the editor instance using the useTiptap hook.

Using useTiptap

The useTiptap hook returns the editor instance and an isReady flag that indicates whether the editor has finished initializing.

import { useTiptap } from '@tiptap/react'

function MenuBar() {
  const { editor, isReady } = useTiptap()

  if (!isReady || !editor) {
    return null
  }

  return (
    <div className="menu-bar">
      <button
        onClick={() => editor.chain().focus().toggleBold().run()}
        className={editor.isActive('bold') ? 'is-active' : ''}
      >
        Bold
      </button>
      <button
        onClick={() => editor.chain().focus().toggleItalic().run()}
        className={editor.isActive('italic') ? 'is-active' : ''}
      >
        Italic
      </button>
    </div>
  )
}

Then include the menu bar in your editor:

<Tiptap instance={editor}>
  <MenuBar />
  <Tiptap.Content />
</Tiptap>

Using useTiptapState for reactive state

For performance-sensitive components, use useTiptapState to subscribe to specific parts of the editor state. This prevents unnecessary re-renders when unrelated state changes.

import { useTiptap, useTiptapState } from '@tiptap/react'

function WordCount() {
  const { isReady } = useTiptap()

  const wordCount = useTiptapState((state) => {
    const text = state.editor.state.doc.textContent
    return text.split(/\s+/).filter(Boolean).length
  })

  if (!isReady) {
    return null
  }

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

The selector function receives an EditorStateSnapshot and should return only the data your component needs. The component will only re-render when the selected value changes.

Important

Only use useTiptapState when the editor is ready. Check isReady from useTiptap() before rendering components that use this hook.

Alternative: Manual setup with EditorContent

For cases where you need more control over the editor setup, you can use EditorContent directly:

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

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

  return (
    <>
      <EditorContent editor={editor} />
      <FloatingMenu editor={editor}>This is the floating menu</FloatingMenu>
      <BubbleMenu editor={editor}>This is the bubble menu</BubbleMenu>
    </>
  )
}

export default Editor

This approach requires manually passing the editor prop to each component. The <Tiptap> component is preferred for most use cases as it reduces boilerplate and provides context automatically.

Using the EditorContext

The <Tiptap> component automatically provides the EditorContext, which means you can also use the useCurrentEditor hook inside it for backwards compatibility with existing code.

import { useCurrentEditor } from '@tiptap/react'

function EditorJSONPreview() {
  const { editor } = useCurrentEditor()

  if (!editor) {
    return null
  }

  return <pre>{JSON.stringify(editor.getJSON(), null, 2)}</pre>
}

For new code, prefer using useTiptap() which provides additional context like the isReady flag.

Reacting to editor state changes

To react to editor state changes without causing unnecessary re-renders, use the useEditorState hook:

import { useEditorState } from '@tiptap/react'

function EditorStateDisplay({ editor }) {
  const editorState = useEditorState({
    editor,
    selector: ({ editor }) => {
      if (!editor) return null

      return {
        isEditable: editor.isEditable,
        isBold: editor.isActive('bold'),
        isItalic: editor.isActive('italic'),
      }
    },
  })

  return <div>Bold active: {editorState?.isBold ? 'Yes' : 'No'}</div>
}

Tip

When using the <Tiptap> component, prefer useTiptapState which automatically uses the editor from context.

Use SSR with React and Tiptap

Tiptap can be used with server-side rendering (SSR) in React applications. To ensure that the editor is only initialized on the client side, use the immediatelyRender option:

'use client'

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

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

  return (
    <Tiptap instance={editor}>
      <Tiptap.Loading>
        <div className="skeleton">Loading editor...</div>
      </Tiptap.Loading>
      <Tiptap.Content />
    </Tiptap>
  )
}

The Tiptap.Loading component is particularly useful with SSR as it displays a placeholder until the editor initializes on the client.

Optimize your performance

We recommend visiting the React Performance Guide to integrate the Tiptap Editor efficiently. This will help you avoid potential issues as your app scales.

API Reference

Tiptap component

The root provider component that makes the editor instance available via React context.

PropTypeDescription
instanceEditor | nullThe editor instance from useEditor()
childrenReactNodeChild components

useTiptap hook

Returns the Tiptap context value.

const { editor, isReady } = useTiptap()
PropertyTypeDescription
editorEditor | nullThe editor instance
isReadybooleantrue when the editor has finished initializing

useTiptapState hook

Subscribes to a slice of the editor state using a selector function.

const value = useTiptapState(selector, equalityFn?)
ParameterTypeDescription
selector(state: EditorStateSnapshot) => TFunction to select state
equalityFn(a: T, b: T) => booleanOptional equality function to control re-renders

Next steps