Skip to main content
The History extension (also known as UndoRedo) provides undo and redo functionality for your editor. It tracks changes and allows users to step backward and forward through their edit history.
If you’re using the @tiptap/extension-collaboration package, do not use this extension. The Collaboration extension includes its own history implementation that’s designed to work in collaborative environments.

Installation

The History extension is included in the @tiptap/extensions package and is also part of the StarterKit.
npm install @tiptap/extensions

Basic Usage

Included in StarterKit

The History extension is included by default when using StarterKit:
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'

const editor = new Editor({
  extensions: [
    StarterKit, // History is included
  ],
})

Standalone Usage

import { Editor } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { UndoRedo } from '@tiptap/extensions/undo-redo'

const editor = new Editor({
  extensions: [
    Document,
    Paragraph,
    Text,
    UndoRedo,
  ],
})

Custom Configuration

import StarterKit from '@tiptap/starter-kit'

const editor = new Editor({
  extensions: [
    StarterKit.configure({
      history: {
        depth: 50,
        newGroupDelay: 1000,
      },
    }),
  ],
})

Configuration Options

depth
number
The amount of history events that are collected before the oldest events are discarded. This prevents the history from growing indefinitely.Default: 100
UndoRedo.configure({
  depth: 50, // Keep only last 50 changes
})
newGroupDelay
number
The delay in milliseconds between changes after which a new history group should be started. Changes that occur within this delay are grouped together as a single undo/redo step.Default: 500
UndoRedo.configure({
  newGroupDelay: 1000, // Group changes within 1 second
})

Commands

undo
command
Undo recent changes.Keyboard shortcuts: Cmd/Ctrl + Z, Cmd/Ctrl + я (Russian layout)
editor.commands.undo()
redo
command
Reapply reverted changes.Keyboard shortcuts:
  • Cmd/Ctrl + Shift + Z
  • Cmd/Ctrl + Y
  • Cmd/Ctrl + Shift + я (Russian layout)
editor.commands.redo()

Keyboard Shortcuts

ShortcutCommandDescription
Cmd/Ctrl + ZundoUndo the last change
Cmd/Ctrl + Shift + ZredoRedo the last undone change
Cmd/Ctrl + YredoRedo the last undone change
Cmd/Ctrl + яundoUndo (Russian keyboard layout)
Cmd/Ctrl + Shift + яredoRedo (Russian keyboard layout)

Advanced Examples

Custom Undo/Redo Buttons

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'

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

// Create undo button
const undoButton = document.querySelector('#undo')
undoButton.addEventListener('click', () => {
  editor.commands.undo()
})

// Create redo button
const redoButton = document.querySelector('#redo')
redoButton.addEventListener('click', () => {
  editor.commands.redo()
})

// Update button states
editor.on('transaction', () => {
  undoButton.disabled = !editor.can().undo()
  redoButton.disabled = !editor.can().redo()
})

React Component with Undo/Redo

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

function Editor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Start typing...</p>',
  })
  
  const [canUndo, setCanUndo] = useState(false)
  const [canRedo, setCanRedo] = useState(false)
  
  useEffect(() => {
    if (!editor) return
    
    const updateHistoryState = () => {
      setCanUndo(editor.can().undo())
      setCanRedo(editor.can().redo())
    }
    
    editor.on('transaction', updateHistoryState)
    updateHistoryState()
    
    return () => {
      editor.off('transaction', updateHistoryState)
    }
  }, [editor])
  
  return (
    <div>
      <div className="toolbar">
        <button 
          onClick={() => editor.commands.undo()} 
          disabled={!canUndo}
        >
          Undo
        </button>
        <button 
          onClick={() => editor.commands.redo()} 
          disabled={!canRedo}
        >
          Redo
        </button>
      </div>
      <EditorContent editor={editor} />
    </div>
  )
}

Shorter History Depth for Memory Efficiency

const editor = new Editor({
  extensions: [
    StarterKit.configure({
      history: {
        depth: 20, // Keep only 20 steps instead of default 100
      },
    }),
  ],
})

Faster Grouping for Rapid Edits

// Useful for live coding editors or fast-paced note-taking
const editor = new Editor({
  extensions: [
    StarterKit.configure({
      history: {
        newGroupDelay: 200, // Group changes within 200ms
      },
    }),
  ],
})

Disable History in StarterKit

If you want to use the Collaboration extension instead:
import StarterKit from '@tiptap/starter-kit'
import Collaboration from '@tiptap/extension-collaboration'
import * as Y from 'yjs'

const ydoc = new Y.Doc()

const editor = new Editor({
  extensions: [
    StarterKit.configure({
      history: false, // Disable default history
    }),
    Collaboration.configure({
      document: ydoc,
    }),
  ],
})

Check if Undo/Redo is Available

// Check if undo is possible
if (editor.can().undo()) {
  console.log('Undo is available')
  editor.commands.undo()
}

// Check if redo is possible
if (editor.can().redo()) {
  console.log('Redo is available')
  editor.commands.redo()
}

Vue Component Example

<template>
  <div>
    <div class="toolbar">
      <button @click="undo" :disabled="!canUndo">
        Undo
      </button>
      <button @click="redo" :disabled="!canRedo">
        Redo
      </button>
    </div>
    <editor-content :editor="editor" />
  </div>
</template>

<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

export default {
  components: {
    EditorContent,
  },
  data() {
    return {
      editor: null,
      canUndo: false,
      canRedo: false,
    }
  },
  mounted() {
    this.editor = new Editor({
      extensions: [StarterKit],
      content: '<p>Start typing...</p>',
      onTransaction: () => {
        this.canUndo = this.editor.can().undo()
        this.canRedo = this.editor.can().redo()
      },
    })
  },
  beforeUnmount() {
    this.editor.destroy()
  },
  methods: {
    undo() {
      this.editor.commands.undo()
    },
    redo() {
      this.editor.commands.redo()
    },
  },
}
</script>

How History Works

The History extension groups changes based on time. When you type continuously, all changes within the newGroupDelay window are grouped together as a single undo step. This creates a natural editing experience:
  • Typing continuously: All text entered within 500ms is one undo step
  • Pausing then typing: Creates a new undo step
  • Each formatting change: Usually a separate undo step
  • Large operations: Typically a single undo step

Source Code

View the source code on GitHub:

Build docs developers (and LLMs) love