The drawing state machine and the save/restore stack
Immediate-mode commands take no style arguments — fill() doesn't say what color, stroke() doesn't say how wide — because every visual setting lives as sticky state sitting in the context object, and save()/restore() exist to push and pop that entire pile of state as one unit so a transform or clip you set for one shape can't leak into the next.
The drawing state machine and the save/restore stack
Back in the immediate-mode lesson, consequence 3 was this: ctx.fill() paints using whatever color is already sitting in ctx.fillStyle, because a command that paints instantly with no arguments has nowhere else to get its style from. That's not an isolated quirk of fillStyle — it's the whole context. Every visual setting on the canvas works the same way, and once you see the full pile of state, save() and restore() stop looking like housekeeping trivia and start looking like the one tool that keeps that pile from leaking between shapes that have no business sharing it.
The full pile of sticky state
The context object (ctx) holds a set of properties that every subsequent drawing command reads implicitly. None of them are passed as arguments to fill(), stroke(), or drawImage() — they're just sitting there, and they stay set until something reassigns them:
- Fill and stroke:
fillStyle,strokeStyle,lineWidth,lineCap,lineJoin,miterLimit - Text:
font,textAlign,textBaseline - Compositing:
globalAlpha,globalCompositeOperation(covered in full in compositing and blending) - Geometry of the space itself: the current transform matrix — every
translate/rotate/scale/setTransformcall accumulates into one matrix that every coordinate you draw is silently run through (see the transform matrix) - The clip region: whatever
ctx.clip()last carved out, which silently confines every subsequent paint operation to that region (see compositing and blending) - Shadows:
shadowColor,shadowBlur,shadowOffsetX,shadowOffsetY
ctx.fillStyle = "red";
ctx.lineWidth = 5;
ctx.fillRect(0, 0, 50, 50); // red fill
ctx.fillRect(60, 0, 50, 50); // STILL red — fillStyle never changed
// STILL uses lineWidth 5 if you stroke itThis is the same shape as WebGL's state machine: a pile of current settings that a stateless "go" command (fill(), drawArrays()) silently reads from at the moment it fires. WebGL binds buffers and programs into slots; the canvas context sets properties directly onto itself — but the mental model is identical: nothing you draw carries its own configuration, all of it is ambient, and every command uses whatever happens to be ambient right now. Immediate mode forces this shape, because a command that paints instantly with no retained object has no other place to keep its settings between calls.
The problem: state leaks across shapes
Because every one of those properties is sticky and global to the context, drawing one shape with a rotation, a clip, or a special style risks contaminating the next shape too, unless you manually reset every property you touched:
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
ctx.fillRect(0, 0, 50, 50); // a rotated, offset square
// Oops — still rotated and translated:
ctx.fillRect(0, 0, 50, 50); // this square is ALSO rotated 45deg,
// because the transform never resetManually undoing a transform (ctx.rotate(-Math.PI / 4); ctx.translate(-100, -100)) works but is fragile and easy to get subtly wrong, especially once several properties are involved at once. What's actually wanted is a way to say "everything I'm about to change, undo it all afterward" — as one operation, not a hand-written inverse of each individual change.
save() and restore(): a stack for the whole pile at once
ctx.save() pushes a copy of the entire current drawing state — every property listed above, all at once — onto an internal stack. ctx.restore() pops the most recently pushed copy back off, and every one of those properties snaps back to whatever it was at the matching save(), in a single call:
ctx.fillStyle = "black";
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
ctx.save(); // push: {fillStyle: black, transform: translated+rotated, ...}
ctx.fillStyle = "red";
ctx.rotate(Math.PI / 4); // now rotated 90deg total
ctx.fillRect(0, 0, 50, 50); // red, rotated 90deg from origin
ctx.restore(); // pop: back to {fillStyle: black, transform: translated+rotated 45deg only}
ctx.fillRect(0, 0, 50, 50); // black again, rotated only 45deg —
// exactly as it was before the save()The pattern this enables is the workhorse of nearly all non-trivial canvas code: save → locally change transform/clip/style → draw → restore. You never have to compute or remember the inverse of what you changed — restore() snaps everything back in one shot, regardless of how many properties you touched in between.
function drawRotatedSquare(x, y, angleRadians, color) {
ctx.save(); // isolate everything below from the caller's state
ctx.translate(x, y);
ctx.rotate(angleRadians);
ctx.fillStyle = color;
ctx.fillRect(-25, -25, 50, 50);
ctx.restore(); // undo translate, rotate, and fillStyle in one call —
// the path drawn and the pixels painted are untouched
}
drawRotatedSquare(100, 100, Math.PI / 6, "red");
drawRotatedSquare(200, 150, Math.PI / 3, "blue"); // starts clean —
// no leftover rotation from the call aboveNesting: a real stack, not a single slot
Because it's a genuine stack, save() can be called multiple times before a matching restore(), and each restore() pops exactly one level — which is what makes nested transforms (a shape drawn relative to a parent that's relative to another parent) tractable:
ctx.save(); // level 1
ctx.translate(100, 100);
ctx.save(); // level 2
ctx.rotate(Math.PI / 4);
ctx.fillRect(0, 0, 20, 20); // translated AND rotated
ctx.restore(); // back to level 1: translated only, rotation undone
ctx.fillRect(0, 0, 20, 20); // translated, NOT rotated
ctx.restore(); // back to level 0: neither translated nor rotatedWhat save/restore does NOT touch
This is the most common misconception about restore(), and it's worth stating precisely: save()/restore() only cover the drawing state listed at the top of this lesson. They do not touch two other things:
- The current path — the sequence of
moveTo/lineTo/arc/etc. calls building up toward the nextfill()orstroke()(the full mechanics of paths are the next lesson). A path under construction when you callsave()is still under construction afterrestore()— building it is not part of the saved state at all. - The bitmap contents — the actual pixels already painted into the backing store from the pixel backing store.
restore()changes how future commands will paint; it does not erase or repaint a single pixel that's already there. If you drew a red square and then calledsave(), changedfillStyle, drew a blue square, and calledrestore(), both squares are still on the canvas —restore()revertedfillStyleback to red for whatever you draw next, it did not undo the blue square that already got painted.
ctx.fillStyle = "red";
ctx.save();
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, 50, 50); // blue square painted onto the bitmap — permanent
ctx.restore(); // fillStyle back to red — but the blue square is still there!
ctx.fillRect(60, 0, 50, 50); // this one is red
// Both squares remain on screen. restore() undid the STATE, not the PIXELS.restore() is not an undo button for your drawing. It's an undo button for your settings.
Where this goes next
Two of the properties on that state list — the current path and the transform matrix — are big enough to deserve their own treatment. Paths: the two-phase model covers how a path is built up across several calls before a single fill() or stroke() commits it, which is exactly the "state that save/restore doesn't touch" gap this lesson just pointed at.
Go deeper
- MDN — CanvasRenderingContext2D.save() — The authoritative list of exactly which properties are pushed onto the drawing state stack, straight from the spec-derived reference.
- MDN — CanvasRenderingContext2D.restore() — Confirms restore() pops the most recently saved state and clarifies its behavior when the stack is empty.
- HTML spec — the canvas drawing state — The normative definition of what counts as 'drawing state' for save/restore purposes versus what's explicitly excluded, like the current path.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- List at least six properties that live as sticky state on the canvas context, and explain why fill() and stroke() don't take them as arguments.
- How is the canvas context's sticky state the same shape as the WebGL state machine from the graphics pipeline module?
- What exactly does ctx.save() push, and what does ctx.restore() do with it?
- Walk through the nested save/save/restore/restore example and say what the transform is at each restore point.
- What happens if a function calls save() but returns early without calling the matching restore(), and why does the canvas give no error when this happens?
- Name the two things save()/restore() explicitly do NOT cover, and explain why restore() does not 'undo' a shape you already painted.
- Describe the save -> locally change transform/clip/style -> draw -> restore pattern in your own words, and say what problem it solves.