Canvas Clipping

Learn how clip() carves the canvas into a restricted drawing region, letting you mask images, panels, and reveal animations into any shape.

What clip() Does

clip() takes whatever path you most recently built with beginPath(), moveTo/lineTo/arc, etc., and turns it into a mask: every drawing operation that happens afterward — fillRect, drawImage, gradients, text — only affects pixels that fall inside that path. Anything outside the path is left completely untouched, as if it were never drawn.

Example 1: Circular Clip Around a Photo

<!DOCTYPE html>
<html>
<body>

<canvas id="scene" width="300" height="300" style="border:1px solid #ccc;"></canvas>

<script>
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');

const photo = new Image();
photo.src = 'assets/photo.jpg';

photo.onload = () => {
  ctx.save();

  ctx.beginPath();
  ctx.arc(150, 150, 80, 0, Math.PI * 2);
  ctx.clip(); // everything below is confined to this circle

  ctx.drawImage(photo, 70, 70, 160, 160); // the square photo is cropped to a circle

  ctx.restore(); // remove the clip so later drawing is unrestricted
};
</script>

</body>
</html>

Clipping Regions Intersect, They Don't Replace

Calling clip() a second time does not swap in a new region — it intersects the new path with whatever clip is already active, so the visible area can only shrink, never grow, until you restore() back to a wider (or absent) clip. This makes clipping composable: you can narrow a large region down step by step, for example clipping to a rounded rectangle and then further clipping to a diagonal band inside it.

Example 2: Rounded-Rectangle Avatar Mask

<!DOCTYPE html>
<html>
<body>

<canvas id="scene" width="200" height="200" style="border:1px solid #ccc;"></canvas>

<script>
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');

function clipRoundedRect(x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
  ctx.clip();
}

const avatar = new Image();
avatar.src = 'assets/avatar.jpg';

avatar.onload = () => {
  ctx.save();
  clipRoundedRect(40, 40, 120, 120, 18);
  ctx.drawImage(avatar, 40, 40, 120, 120);
  ctx.restore();
};
</script>

</body>
</html>

Clipping vs. Compositing vs. CSS overflow

clip() is a canvas-only, pixel-precise mask that works with any path shape, including curves and text outlines, and it costs nothing extra to combine with transformations. CSS overflow: hidden only clips to rectangles and only affects DOM boxes, not canvas drawing operations. The globalCompositeOperation value 'destination-in' achieves a similar masking look by blending two already-drawn layers together instead of restricting a path — reach for clip() when you know the mask shape in advance, and compositing when the mask itself is a drawn image or gradient.

Example 3: Clipped Photo Grid

<!DOCTYPE html>
<html>
<body>

<canvas id="scene" width="330" height="220" style="border:1px solid #ccc;"></canvas>

<script>
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');

const thumbSize = 100;
const gap = 10;

const photos = [
  'assets/photo1.jpg',
  'assets/photo2.jpg',
  'assets/photo3.jpg',
  'assets/photo4.jpg',
  'assets/photo5.jpg',
  'assets/photo6.jpg'
].map((src) => Object.assign(new Image(), { src }));

Promise.all(photos.map((photo) => new Promise((resolve) => {
  photo.onload = resolve;
}))).then(() => {
  photos.forEach((photo, i) => {
    const x = (i % 3) * (thumbSize + gap);
    const y = Math.floor(i / 3) * (thumbSize + gap);

    ctx.save();
    ctx.beginPath();
    ctx.rect(x, y, thumbSize, thumbSize);
    ctx.clip();
    ctx.drawImage(photo, x, y, thumbSize, thumbSize);
    ctx.restore();
  });
});
</script>

</body>
</html>
ConceptPurpose
ctx.clip()Intersects the current clipping region with the current path; restricts all later drawing to that area.
new Path2D()Lets you build a reusable path object once and pass it to clip(path) or clip(path, 'evenodd') without rebuilding it each frame.
'evenodd' fill ruleAn optional second argument to clip() that changes how overlapping sub-paths combine, useful for donut/ring shapes with holes.
Note: Because clip() can only shrink the drawable area and never resets on its own, always open a save() before clipping and close it with restore() once you're done — otherwise every future draw call in that frame stays trapped inside the last shape you clipped to.
  • Circular or rounded-rectangle avatar and thumbnail masks
  • Custom-shaped panels and cards that aren't plain rectangles
  • Reveal/wipe transition animations that grow a clip region over time
  • Cropping a video frame or sprite into a non-rectangular viewport
  • Confining a busy background pattern or texture to a text outline

Exercise: Canvas Clipping

What does calling clip() do on the current path?