Undo/Redo History Toolbar with Jump-to-Any-State

Undo/Redo History Toolbar with Jump-to-State · Misc · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Built on the standard snapshot-array-plus-pointer pattern, not fragile per-action "inverse operation" logic
Undo, redo, and jump-to-any-history-entry are all the same underlying operation: move the pointer, re-render
Correctly discards the abandoned "future" branch when a new action happens after undoing, matching standard undo/redo behavior
Full keyboard shortcut support: Ctrl/Cmd+Z for undo, Ctrl/Cmd+Shift+Z for redo
Toggleable history list showing every past state with a one-click jump to any specific point
Undo/redo buttons correctly disable themselves at the boundaries of available history
Zero per-action-type special-casing required — the same logic handles any number of distinct action types uniformly

About this UI Snippet

Undo/Redo History — The Pointer-Into-a-Snapshot-Array Pattern

Screenshot of the Undo/Redo History Toolbar with Jump-to-State snippet rendered live

A genuinely correct undo/redo implementation is a specific, well-understood pattern — not a stack of "reverse operations" that has to be carefully written for every possible action type. This snippet implements the simpler, more robust version: an array of full state snapshots, plus a single integer pointer into that array. Every operation — undo, redo, or jumping to an arbitrary point in history — reduces to nothing more than moving the pointer and re-rendering.

Why snapshots, not "reverse operations"

A tempting alternative design stores each action as an *inverse* operation (e.g. "delete the last shape" as the undo for "add a shape") — but this requires writing and maintaining a correct inverse for every single action type in the app, and inverses can be genuinely hard to get right for complex or composite actions. Storing a full snapshot of the *entire relevant state* after every action sidesteps this completely: undoing is never "run the opposite operation," it's simply "look at what the state used to be, one step back," which is trivially correct by construction no matter how complex an individual action was.

The pointer, not the array length, defines "where you are"

pointer is a separate integer tracking the currently-displayed index into history, distinct from the array's own length. undo() and redo() do nothing more than decrement or increment pointer (bounded at the array's edges) and call render() — there's no separate "apply the inverse" logic, because render() always just displays whatever full snapshot lives at history[pointer].

Why a new action must discard the "future" branch

pushState() calls history.slice(0, pointer + 1) *before* appending the new state — this is the detail that makes branching behave correctly. If a user undoes three steps and then performs a brand-new action, the three steps they'd undone past are no longer reachable via redo; they represent an abandoned timeline the new action has now diverged from. Without this truncation, the old "future" states would linger in the array past the new action, and redo could jump to a state that's no longer consistent with what's actually been done since.

Jumping to an arbitrary history entry is not a special case

The history list lets a user click any past entry and jump straight to it — implemented as nothing more than setting pointer directly to that entry's index and calling render(), the exact same two operations undo()/redo() perform. This is a direct consequence of the pointer-into-an-array design: there's no meaningful difference between "move the pointer back one step" and "move the pointer back five steps," so no separate jump-specific logic is needed at all.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain in detail why the snapshot-array-plus-pointer undo pattern avoids the correctness problems of a reverse-operations approach, and to walk through exactly what happens to the history array's contents when a new action is pushed after several undo steps. It's also worth asking for a version that coalesces rapid successive actions (like fast typing) into fewer history entries to avoid an excessively long, granular history, or one that caps the maximum history length and discards the oldest entries once that cap is exceeded.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an undo/redo history toolbar in HTML, CSS, and vanilla JavaScript — no external library.

Requirements:
- A simple interactive canvas area where clicking adds a new visual element (like a small shape) at the click position, and a toolbar with Undo and Redo buttons plus a keyboard shortcut (Ctrl/Cmd+Z for undo, Ctrl/Cmd+Shift+Z for redo).
- Implement the undo/redo system using an array of full state snapshots (not a list of reverse/inverse operations per action type) plus a single pointer index into that array — every state-changing action must push a complete new snapshot onto the array.
- Undo and redo must both work by simply moving the pointer backward or forward by one and re-rendering whatever full snapshot is at that new pointer position — there should be no separate "apply the inverse of this specific action" logic anywhere.
- When a new action is performed after the user has undone one or more steps, correctly discard the now-abandoned "future" portion of the history array (everything beyond the current pointer) before appending the new state, so redo is no longer available past that new action.
- Include a toggleable history panel listing every past state, where clicking any entry jumps the pointer directly to that point in history using the exact same underlying mechanism as a single undo or redo step (not separate jump-specific logic).
- Disable the Undo button when the pointer is at the very first history entry, and disable the Redo button when the pointer is at the very last entry.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Click anywhere on the canvasAdds a new shape and pushes a new state snapshot onto the history array, moving the pointer to it.
  2. 2
    Click Undo (or press Ctrl/Cmd+Z)Moves the pointer back one step and re-renders whatever snapshot lives there — the most recently added shape disappears.
  3. 3
    Click Redo (or press Ctrl/Cmd+Shift+Z)Moves the pointer forward one step, restoring whatever was undone, as long as no new action has happened since.
  4. 4
    Add a new shape after undoingThe abandoned "future" states are discarded — redo is no longer available past this new action, matching standard undo/redo behavior everywhere.
  5. 5
    Open the History panel and click any entryJumps the pointer directly to that specific point in history — implemented as exactly the same operation undo/redo use internally.

Real-world uses

Common Use Cases

EDITOR
Canvas, drawing, and design tools
Any tool where users place, move, or modify visual elements benefits directly from this exact snapshot-based undo model.
Complex multi-field form editors
Form builders or content editors with many possible edit actions benefit from snapshot-based undo over maintaining inverse operations per field type.
TEXT
Rich text and document editors
Document state snapshots (even if throttled/coalesced for typing) follow the same pointer-based undo/redo architecture.
Turn-based game state history
Games with a turn history benefit from the same jump-to-any-point pattern, letting a player review or rewind to any prior turn.

Got questions?

Frequently Asked Questions

Reverse-operation undo requires writing and correctly maintaining an inverse for every distinct action type in the app, which becomes error-prone for complex actions. Full snapshots sidestep this entirely — undoing is just "display what the state was one step back," which is correct by construction regardless of how complicated the action that produced each state was.

The history array is truncated to everything up to and including the current pointer position before the new state is appended — the abandoned "future" states (everything you had undone past) are discarded, and redo is no longer available past the new action, exactly matching how undo/redo behaves in real applications.

No — it uses the exact same underlying mechanism: setting the pointer to a specific index and re-rendering. There is no separate jump-specific code path, since moving the pointer by one step (undo/redo) and moving it by several steps (a history-list jump) are fundamentally the same operation.

For high-frequency actions like keystrokes, a real implementation typically coalesces rapid successive changes into a single history entry (for example, committing a new snapshot only after a pause in typing) rather than pushing a new full snapshot per keystroke — the underlying pointer/array pattern stays the same either way.

Undo disables when the pointer is at index 0 (the very first, empty state) — there's nothing earlier to move back to. Redo disables when the pointer is at the last index in the history array — there's no "future" state beyond the current one to move forward to.

Keep the same pattern — every action (move, resize, delete, add) simply computes and pushes a new full snapshot of the relevant state after that action, exactly like the single addShape action does here. No per-action-type undo logic needs to be written.