Scenario

Undo/redo stack management is inconsistent

The undo/redo stack in contenteditable elements behaves inconsistently across browsers. Programmatic DOM changes may or may not be added to the undo stack, and the stack may be cleared unexpectedly. Custom undo/redo implementation is often necessary.

other
Scenario ID
scenario-undo-redo-stack

Details

The undo/redo stack in contenteditable elements behaves inconsistently across browsers. Programmatic DOM changes may or may not be added to the undo stack, and the stack may be cleared unexpectedly. Custom undo/redo implementation is often necessary.

Observed Behavior

Scenario 1: Programmatic DOM changes

  • Chrome/Edge: Changes may not be added to undo stack
  • Firefox: Similar behavior
  • Safari: Undo stack handling varies

Scenario 2: preventDefault() operations

  • Chrome/Edge: Custom operations may not be in undo stack
  • Firefox: Similar issues
  • Safari: Undo stack most inconsistent

Scenario 3: Stack clearing

  • Chrome/Edge: Stack may be cleared on focus changes
  • Firefox: Similar behavior
  • Safari: Stack clearing most unpredictable

Scenario 4: Multiple undo operations

  • Chrome/Edge: May undo multiple operations at once
  • Firefox: Similar behavior
  • Safari: Undo granularity varies

Impact

  • Loss of undo history
  • Inability to undo custom operations
  • Unexpected stack clearing
  • Need for custom undo/redo implementation

Browser Comparison

  • Chrome/Edge: Generally better undo stack handling
  • Firefox: More likely to lose undo history
  • Safari: Most inconsistent undo behavior

Workaround

Implement custom undo/redo:

class UndoRedoManager {
  constructor(element) {
    this.element = element;
    this.undoStack = [];
    this.redoStack = [];
    this.maxStackSize = 50;
  }
  
  saveState() {
    const state = {
      html: this.element.innerHTML,
      selection: this.saveSelection()
    };
    
    this.undoStack.push(state);
    if (this.undoStack.length > this.maxStackSize) {
      this.undoStack.shift();
    }
    this.redoStack = []; // Clear redo stack on new action
  }
  
  undo() {
    if (this.undoStack.length === 0) return;
    
    const currentState = {
      html: this.element.innerHTML,
      selection: this.saveSelection()
    };
    this.redoStack.push(currentState);
    
    const previousState = this.undoStack.pop();
    this.restoreState(previousState);
  }
  
  redo() {
    if (this.redoStack.length === 0) return;
    
    const currentState = {
      html: this.element.innerHTML,
      selection: this.saveSelection()
    };
    this.undoStack.push(currentState);
    
    const nextState = this.redoStack.pop();
    this.restoreState(nextState);
  }
  
  restoreState(state) {
    this.element.innerHTML = state.html;
    this.restoreSelection(state.selection);
  }
  
  saveSelection() {
    const selection = window.getSelection();
    if (selection.rangeCount === 0) return null;
    const range = selection.getRangeAt(0);
    return {
      startContainer: range.startContainer,
      startOffset: range.startOffset,
      endContainer: range.endContainer,
      endOffset: range.endOffset
    };
  }
  
  restoreSelection(saved) {
    if (!saved) return;
    try {
      const range = document.createRange();
      range.setStart(saved.startContainer, saved.startOffset);
      range.setEnd(saved.endContainer, saved.endOffset);
      const selection = window.getSelection();
      selection.removeAllRanges();
      selection.addRange(range);
    } catch (e) {
      // Selection invalid, ignore
    }
  }
}

const undoRedo = new UndoRedoManager(element);

element.addEventListener('input', () => {
  undoRedo.saveState();
});

// Handle Ctrl+Z and Ctrl+Y
element.addEventListener('keydown', (e) => {
  if (e.ctrlKey || e.metaKey) {
    if (e.key === 'z' && !e.shiftKey) {
      e.preventDefault();
      undoRedo.undo();
    } else if ((e.key === 'y') || (e.key === 'z' && e.shiftKey)) {
      e.preventDefault();
      undoRedo.redo();
    }
  }
});

References

Scenario flow

Visual view of how this scenario connects to its concrete cases and environments. Nodes can be dragged and clicked.

React Flow mini map

Variants

Each row is a concrete case for this scenario, with a dedicated document and playground.

Case OS Device Browser Keyboard Status
ce-0113-undo-redo-custom-ops Windows 11 Desktop or Laptop Any Chrome 120.0 US draft
ce-0129-undo-redo-stack-cleared Windows 11 Desktop or Laptop Any Chrome 120.0 US draft
ce-0141-undo-redo-multiple-ops Windows 11 Desktop or Laptop Any Chrome 120.0 US draft
ce-0150-undo-redo-custom-formatting Windows 11 Desktop or Laptop Any Chrome 120.0 US draft
ce-0173-undo-redo-custom-text-insert Windows 11 Desktop or Laptop Any Chrome 120.0 US draft

Browser compatibility

This matrix shows which browser and OS combinations have documented cases for this scenario. Click on a cell to view the specific case.

Confirmed
Draft
No case documented

Cases

Open a case to see the detailed description and its dedicated playground.

Related Scenarios

Other scenarios that share similar tags or category.

Tags: undo, redo, history

Firefox undo stack corrupted when DOM mutates during editing

In Firefox, programmatic DOM changes during typing (auto-formatting, spellcheck fixes, framework reconciliation) can desynchronize the internal undo stack. Undo/redo may jump to wrong snapshots or truncate history.

1 case
Tags: undo, redo, history

Undo and redo during IME composition

Pressing Undo or Redo while IME composition is active can cancel composition, leave partial syllables, or corrupt the undo stack. Behavior differs by browser and by whether the editor uses native undo or a custom history layer.

3 cases
Tags: undo, redo

Undo and redo behavior is inconsistent across browsers

The undo and redo functionality (Ctrl+Z / Ctrl+Y or Cmd+Z / Cmd+Shift+Z) behaves differently across browsers. Some browsers undo individual keystrokes, while others undo larger operations. The undo stack may also be cleared unexpectedly.

2 cases
Tags: undo, redo

Undo stack and programmatic DOM changes

Programmatic inserts or execCommand during typing can split undo transactions or clear the stack—browser-specific rules differ from custom editor history.

1 case

Comments & Discussion

Have questions, suggestions, or want to share your experience? Join the discussion below.