Skip to content
JavaScript js canvas 5 min read

Canvas State: save & restore

The 2D canvas keeps a single, mutable “drawing state” — every fill color, stroke width, font, and transformation you set sticks around until you change it again. As drawings grow, that global mutability becomes a liability: a rotation applied for one shape quietly bends every shape after it. The save() and restore() methods solve this by giving you a stack of state snapshots, so you can change anything temporarily and roll back to exactly where you were.

What lives in the drawing state

When you call ctx.save(), the canvas pushes a snapshot of the current drawing state onto an internal stack. ctx.restore() pops the most recent snapshot and makes it active again. The state captured is everything except the actual pixels on the canvas and the current path — those are not part of the saved state.

Captured by save()Not captured
fillStyle, strokeStyleThe bitmap (pixels already drawn)
lineWidth, lineCap, lineJoin, miterLimitThe current path (subpaths)
lineDashOffset and the dash patternwidth / height of the canvas
font, textAlign, textBaseline, direction
globalAlpha, globalCompositeOperation
shadowColor, shadowBlur, shadowOffsetX/Y
The current transformation matrix
The current clipping region

Because the transform and the clip are on this list, save/restore is the standard tool for scoping a rotation, translation, or clip to one part of your scene.

Basic pairing

Always treat save() and restore() as a matched pair, like opening and closing a bracket. Change whatever you need after save(), draw, then restore() to undo all of those changes at once.

ctx.fillStyle = "navy";
ctx.fillRect(10, 10, 40, 40); // navy

ctx.save();                    // snapshot: fillStyle is "navy"
ctx.fillStyle = "crimson";
ctx.globalAlpha = 0.4;
ctx.fillRect(60, 10, 40, 40);  // translucent crimson
ctx.restore();                 // back to navy, alpha 1

ctx.fillRect(110, 10, 40, 40); // navy again, fully opaque

You never have to manually reset fillStyle or globalAlpharestore() does it for the whole state in one call.

Why it is essential when transforming

Transforms are cumulative. translate, rotate, and scale multiply into the current matrix rather than replacing it, so without isolation a single rotate(0.1) keeps tilting everything drawn afterward. Wrapping each transformed object in save/restore keeps the matrix changes local.

<canvas id="c" width="400" height="200" style="background:#0f172a"></canvas>
<script>
  const ctx = document.getElementById("c").getContext("2d");
  const colors = ["#f87171", "#fbbf24", "#34d399", "#60a5fa", "#a78bfa"];

  colors.forEach((color, i) => {
    ctx.save();                         // isolate this card's transform
    ctx.translate(50 + i * 70, 100);    // move origin to the card center
    ctx.rotate((i - 2) * 0.18);         // tilt only this card
    ctx.fillStyle = color;
    ctx.fillRect(-25, -35, 50, 70);     // draw around the new origin
    ctx.restore();                      // matrix back to identity
  });
</script>

Each card is positioned and rotated independently. Remove the save/restore lines and the rotations compound into an unreadable spiral — proof of why the pairing matters.

The stack is nested (LIFO)

The state stack is last-in, first-out, so calls nest cleanly. Each restore() matches the most recent unmatched save(). This lets a parent transform stay in effect while a child applies and undoes its own additional transform.

<canvas id="scene" width="420" height="220" style="background:#111827"></canvas>
<script>
  const ctx = document.getElementById("scene").getContext("2d");

  ctx.save();                    // outer: scene transform
  ctx.translate(210, 110);       // move to center for everything below

  for (let i = 0; i < 8; i++) {
    ctx.save();                  // inner: per-spoke transform
    ctx.rotate((i / 8) * Math.PI * 2);
    ctx.fillStyle = `hsl(${i * 45}, 80%, 60%)`;
    ctx.fillRect(20, -6, 70, 12); // each spoke radiates from center
    ctx.restore();               // undo only the spoke rotation
  }

  ctx.fillStyle = "#fff";
  ctx.beginPath();
  ctx.arc(0, 0, 14, 0, Math.PI * 2); // hub stays centered, no rotation
  ctx.fill();
  ctx.restore();                 // undo the scene translate
</script>

Output:

A spoked wheel: 8 colored bars radiate from a white center hub.

The outer save keeps the origin centered for the whole wheel; each inner save/restore confines a single spoke’s rotation so the hub is drawn upright.

Unbalanced calls

restore() is forgiving but the imbalance bites later. Calling restore() with an empty stack does nothing (no error, no change). Calling save() without a matching restore() leaves stale state on the stack — usually harmless on its own, but it leaks transforms and styles into whatever runs next, especially inside an animation loop where the leak accumulates frame after frame.

Gotcha: Inside requestAnimationFrame, an extra save() per frame slowly grows the stack and can degrade performance or leak transforms. Audit each frame so every save() has exactly one restore().

A common safety pattern is to wrap risky drawing in try/finally:

function drawWidget(ctx) {
  ctx.save();
  try {
    ctx.translate(100, 100);
    ctx.rotate(Math.PI / 6);
    ctx.fillRect(-20, -20, 40, 40);
  } finally {
    ctx.restore(); // runs even if drawing throws
  }
}

Resetting versus restoring

save/restore is scoped and relative. If you instead want to wipe state entirely, modern browsers expose ctx.reset() (clears the bitmap and resets all state to defaults) and ctx.getTransform() / ctx.setTransform(1, 0, 0, 1, 0, 0) to reset only the matrix. Reach for reset() when starting a fresh scene, but use save/restore for local, nestable isolation during a draw.

Best practices

  • Treat save() and restore() as a balanced pair, one restore() for every save().
  • Call save() immediately before any transform you want to scope, and restore() right after the affected draw.
  • Prefer save/restore over manually re-setting individual properties — it is less error-prone and covers the transform and clip too.
  • Keep saves shallow: do not push state you do not need, and never let saves accumulate across animation frames.
  • Use a try/finally around drawing code that might throw so the stack never leaks.
  • Remember the current path is not saved — call beginPath() to start fresh, not restore().
  • For a full reset of the context, use ctx.reset() or setTransform(1,0,0,1,0,0) rather than spamming restore().
Last updated June 1, 2026
Was this helpful?