--- title: History (undo/redo) created_at: 12/20/2025 updated_at: 12/29/2025 keywords: - undo - redo - history - HistoryManager - marks - batch reviewed_by: steveruizok status: published date: 12/20/2025 readability: 9 voice: 8 completeness: 9 accuracy: 10 notes: '' --- The editor's history system tracks changes to the [store](/sdk-features/store) and provides undo/redo. Changes are organized into batches separated by marks, which act as stopping points, so a complex interaction undoes as one step instead of many. The history manager captures all user-initiated store changes automatically and batches rapid changes into single undo steps. You control which changes are recorded with history options. The default UI binds undo to Cmd/Ctrl+Z and redo to Cmd/Ctrl+Shift+Z. ## How it works The history manager maintains two stacks: one for undos and one for redos. Each stack contains entries that are either diffs (record changes) or marks (stopping points). When you modify the store, the history manager captures the change as a diff. Changes accumulate until you create a mark, then the pending changes are flushed to the undo stack as a single entry. This batching prevents every keystroke or mouse movement from becoming a separate undo step. ```typescript editor.updateShape({ id: myShapeId, type: 'geo', x: 100, y: 100 }) editor.updateShape({ id: myShapeId, type: 'geo', x: 110, y: 100 }) editor.updateShape({ id: myShapeId, type: 'geo', x: 120, y: 100 }) // All three updates are batched together until a mark is created ``` When you undo, the manager reverses all changes back to the previous mark, moves them to the redo stack, and applies the reversed diff atomically. Redo does the inverse. ## Marks and stopping points Marks define where undo and redo operations stop. Create marks with [Editor#markHistoryStoppingPoint](?) at the start of user interactions so that complex operations can be undone in one step. The optional name only shows up in the mark id, which is useful for debugging. ```typescript const markId = editor.markHistoryStoppingPoint('rotate shapes') editor.rotateShapesBy(editor.getSelectedShapeIds(), Math.PI / 4) // Undoing will return to this mark ``` Each mark has a unique identifier that you can use with [bailToMark](#bailing) or [squashToMark](#squashing). Creating a mark flushes pending changes onto the undo stack. It doesn't clear the redo stack; the next recorded change does that. ## Basic operations ### Undo and redo Use [Editor#undo](?) and [Editor#redo](?) to move through history marks. ```typescript editor.undo() // Reverse to previous mark editor.redo() // Reapply changes ``` Both methods return the editor instance for chaining. The [Editor#canUndo](?) and [Editor#canRedo](?) methods are reactive, so you can use them to update UI button states automatically: ```tsx import { useEditor, useValue } from 'tldraw' function UndoButton() { const editor = useEditor() const canUndo = useValue('canUndo', () => editor.canUndo(), [editor]) return ( ) } ``` ### Running operations with history options The [Editor#run](?) method executes a function while controlling how changes affect history. Use it to make changes that don't pollute the undo stack or that preserve the redo stack for special operations. ```typescript // Ignore changes (don't add to undo stack) editor.run( () => { editor.updateShape({ id: myShapeId, type: 'geo', x: 100 }) }, { history: 'ignore' } ) // Record but preserve redo stack editor.run( () => { editor.updateShape({ id: myShapeId, type: 'geo', x: 100 }) }, { history: 'record-preserveRedoStack' } ) ``` The three history modes are: | Mode | Undo stack | Redo stack | | -------------------------- | ---------- | ---------- | | `record` | Add | Clear | | `record-preserveRedoStack` | Add | Keep | | `ignore` | Skip | Keep | > We use `record-preserveRedoStack` when selecting shapes. This way you can undo, select some shapes, copy them, and then redo back to where you were. The selection goes on the undo stack, but existing redos aren't cleared. > We use `ignore` when writing your own pointer position for collaborators to see. Where your cursor was doesn't need to be undoable. (Changes that arrive from other users are marked `'remote'` and are never recorded.) Nested `run` calls keep the outer mode unless they set their own, and no mode is applied while an undo or redo is in progress. ## Advanced features ### Bailing Bailing reverses changes without adding them to the redo stack. The changes are discarded entirely. Use this when canceling an interaction. ```typescript const markId = editor.markHistoryStoppingPoint('begin drag') // User drags shapes around // User presses escape to cancel editor.bailToMark(markId) // Roll back and discard all changes since mark ``` [Editor#bail](?) reverts to the most recent mark. [Editor#bailToMark](?) reverts to a specific mark by ID. > We use bailing while cloning shapes. A user can switch between translating and cloning by pressing or releasing the alt (option) key during a drag. When this changes, we bail on the changes since the interaction started, then apply the new mode's changes. ### Squashing [Editor#squashToMark](?) combines all changes since a mark into a single undo step. Intermediate marks are removed. This simplifies the undo experience for complex multi-step operations. ```typescript const markId = editor.markHistoryStoppingPoint('bump shapes') editor.nudgeShapes(shapes, { x: 10, y: 0 }) editor.nudgeShapes(shapes, { x: 0, y: 10 }) editor.nudgeShapes(shapes, { x: -5, y: -5 }) editor.squashToMark(markId) // All three nudges become one undo step ``` Squashing doesn't change the current state, only how history is organized. If the mark isn't on the undo stack, `squashToMark` logs an error and does nothing. > We use squashing during image cropping. While the user adjusts the crop, each change is recorded and can be undone individually. When the user exits crop mode, we squash the intermediate changes into one history entry. A single undo restores the image to its state before cropping began. ### Clearing history [Editor#clearHistory](?) removes all undo and redo entries. Use this when loading new documents or resetting the editor state. ```typescript editor.loadSnapshot(snapshot) editor.clearHistory() // Start with clean history ``` ## Integration with the store The history manager listens to [store](/sdk-features/store) changes through a history interceptor. It only captures changes with source `'user'`; changes merged from other clients (source `'remote'`) are ignored, and internal writes that shouldn't be undoable use `history: 'ignore'`. Only store records take part in undo/redo; state held outside the store, like your own atoms, does not. The three history modes map onto three internal states: `Recording`, `RecordingPreserveRedoStack`, and `Paused`. The manager pauses itself while applying an undo or redo so those writes don't create new entries. ## Related examples - [Timeline scrubber](/examples/use-cases/timeline-scrubber) - A visual timeline that lets users scrub through document history. - [Store events](/examples/events/store-events) - Listen to store changes, which is how the history manager tracks modifications.