The Canvas API
The Canvas API lets you draw 2D graphics directly in the browser using JavaScript. You get a pixel buffer you can manipulate freely — shapes, text, images, gradients, and raw pixel data. It’s the technology behind interactive charts, game engines, photo editors, data visualizations, and creative coding experiments.
Unlike SVG, where you describe shapes as DOM elements, Canvas gives you an immediate-mode drawing surface. You tell it what to draw, it draws it, and then you move on. That makes Canvas faster for lots of shapes or rapid animation frames, but it also means you have to manage redrawing yourself.
Setting Up a Canvas
You need an HTML <canvas> element and a rendering context to draw on it. The element itself is invisible until you put something on it.
<canvas id="myCanvas" width="400" height="300"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d'); // CanvasRenderingContext2D
</script>
getContext('2d') returns a CanvasRenderingContext2D object, or null if the browser doesn’t support 2D rendering (extremely rare). Always null-check in production code:
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Canvas 2D context not supported');
// Fall back to alternative
}
One thing catches many developers: the canvas width and height attributes set the actual pixel resolution, not the displayed size. If you resize the canvas with CSS only, the browser stretches the pixel buffer, which makes everything look blurry on high-DPI screens. Set the pixel dimensions as attributes (or properties), and use CSS for display sizing:
// Correct: set actual pixel dimensions
canvas.width = 800;
canvas.height = 600;
// Then optionally scale with CSS for layout
canvas.style.width = '400px'; // displays at half size
Once a canvas has a context type, you cannot switch it. Calling getContext('webgl') on a canvas that already has a '2d' context returns null. You get one chance per canvas.
Drawing Rectangles
The Canvas API provides three convenience methods for rectangles:
ctx.fillStyle = '#3498db';
ctx.fillRect(20, 20, 100, 80); // filled rectangle at (20,20), 100x80
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 3;
ctx.strokeRect(140, 20, 100, 80); // stroked (outlined) rectangle
ctx.clearRect(0, 0, canvas.width, canvas.height); // erases everything
fillRect draws immediately — it doesn’t add to a path. strokeRect works the same way. clearRect is useful for animation loops where you redraw every frame from scratch.
Paths and Drawing Order
For anything more complex than rectangles, you build up a path and then fill or stroke it. The sequence is: beginPath() to start fresh, drawing commands to define the shape, then fill() or stroke() to render it.
// Draw a triangle
ctx.beginPath();
ctx.moveTo(70, 200); // move pen to start point without drawing
ctx.lineTo(20, 280); // draw line from current point to (20, 280)
ctx.lineTo(120, 280); // draw line to (120, 280)
ctx.closePath(); // draw line back to the start point
ctx.fillStyle = '#2ecc71';
ctx.fill();
closePath() connects the last point back to the first. You don’t strictly need it before fill() — the fill operation closes the path automatically — but leaving it out before stroke() produces an open shape.
The key gotcha: paths are not cleared between draw calls. If you call fill() and then stroke() without another beginPath(), the stroke draws on top of the fill at the same location. If you’re seeing duplicate shapes, look for a missing beginPath().
Arcs and Curves
// Full circle
ctx.beginPath();
ctx.arc(280, 150, 50, 0, Math.PI * 2); // center (280,150), radius 50
ctx.fillStyle = '#9b59b6';
ctx.fill();
// Arc from 0 to PI (half circle)
ctx.beginPath();
ctx.arc(100, 100, 40, 0, Math.PI);
ctx.stroke();
// Bezier curve — two control points define the curve shape
ctx.beginPath();
ctx.moveTo(50, 200);
ctx.bezierCurveTo(80, 50, 150, 50, 180, 200);
ctx.stroke();
// Quadratic curve — one control point
ctx.beginPath();
ctx.moveTo(50, 200);
ctx.quadraticCurveTo(115, 0, 180, 200);
ctx.stroke();
Angles are measured in radians, where 0 is 3 o’clock, Math.PI is 9 o’clock, and Math.PI * 2 is a full circle. Keep that in mind when rotating things or drawing partial arcs.
Colors and Styles
Beyond solid colors, you can fill and stroke with gradients. Create them with createLinearGradient, createRadialGradient, or createConicGradient, then assign them to fillStyle or strokeStyle:
const linear = ctx.createLinearGradient(0, 0, 400, 0);
linear.addColorStop(0, '#667eea');
linear.addColorStop(1, '#764ba2');
ctx.fillStyle = linear;
ctx.fillRect(20, 20, 360, 80);
const radial = ctx.createRadialGradient(300, 150, 5, 300, 150, 40);
radial.addColorStop(0, '#f093fb');
radial.addColorStop(1, '#f5576c');
ctx.beginPath();
ctx.arc(300, 150, 40, 0, Math.PI * 2);
ctx.fillStyle = radial;
ctx.fill();
globalAlpha sets a transparency level that applies to everything drawn afterward until you reset it:
ctx.globalAlpha = 0.5; // 50% transparent
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);
ctx.globalAlpha = 1.0; // back to fully opaque
Text on Canvas
Draw text with fillText and strokeText. The font property takes any valid CSS font string:
ctx.font = 'bold 24px sans-serif';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Hello Canvas', 200, 60);
textAlign controls horizontal alignment (left, right, center, start, end), and textBaseline controls vertical alignment (top, middle, bottom, alphabetic). These defaults are 'start' and 'alphabetic' respectively, which surprises developers who expect 'center' behavior.
To measure text width before drawing (useful for layout), use measureText:
const metrics = ctx.measureText('Hello Canvas');
const width = metrics.width; // e.g., 126.5
It returns a TextMetrics object with width and some bounding box hints, but it does not give you pixel-perfect character-level positioning. That’s harder than it sounds because fonts are complex.
Drawing Images
Use drawImage to paint an <img>, <canvas>, or <video> element onto the canvas. Three signatures exist for different use cases:
// Draw image at full size at position (dx, dy)
ctx.drawImage(image, dx, dy);
// Draw image scaled to dWidth x dHeight at (dx, dy)
ctx.drawImage(image, dx, dy, dWidth, dHeight);
// Source rectangle (sx, sy, sWidth, sHeight) drawn to dest rectangle
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
const img = new Image();
img.src = 'photo.jpg';
img.onload = () => {
// Draw full image scaled to fit canvas
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
};
The image doesn’t have to be loaded from a URL — you can draw one canvas onto another, which is useful for layering or compositing.
Transforming the Coordinate System
Canvas lets you translate, rotate, and scale the entire drawing surface without changing what you draw:
ctx.save(); // remember current state
ctx.translate(100, 50); // move origin to (100, 50)
ctx.rotate(Math.PI / 6); // rotate 30 degrees
ctx.scale(2, 2); // double the size
// Everything drawn here is translated, rotated, and scaled
ctx.fillRect(0, 0, 50, 50); // actually appears at (100, 50), rotated, 100x100
ctx.restore(); // restore to state before translate/rotate/scale
save() pushes the current state onto a stack. restore() pops it. Call save() before a complex transform and restore() after, and your drawing code doesn’t need to know about the transformation context it operates in.
Transforms are cumulative. If you call translate(50, 0) twice, your origin moves 100 pixels right. Use ctx.resetTransform() to clear all transforms at once.
Pixel Manipulation
Canvas gives you direct access to the pixel buffer. getImageData returns pixel values; putImageData writes them back:
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // Uint8ClampedArray: [r, g, b, a, r, g, b, a, ...]
Each pixel takes 4 consecutive values: red, green, blue, and alpha (0–255 each). To invert colors:
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // red inverted
data[i + 1] = 255 - data[i + 1]; // green inverted
data[i + 2] = 255 - data[i + 2]; // blue inverted
// alpha unchanged
}
ctx.putImageData(imageData, 0, 0);
This kind of pixel-by-pixel access is slow in JavaScript. For real-time image processing on large canvases, consider using WebGL or OffscreenCanvas with a worker thread.
Reusable Paths with Path2D
When you want to draw the same shape multiple times with different transforms, Path2D objects let you define a path once and reuse it:
const arrow = new Path2D();
arrow.moveTo(0, 10);
arrow.lineTo(20, 10);
arrow.lineTo(20, 0);
arrow.lineTo(30, 15);
arrow.lineTo(20, 30);
arrow.lineTo(20, 20);
arrow.lineTo(0, 20);
arrow.closePath();
ctx.fillStyle = '#3498db';
ctx.save();
ctx.translate(50, 50);
ctx.fill(arrow);
ctx.restore();
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 2;
ctx.save();
ctx.translate(150, 50);
ctx.scale(1.5, 1.5);
ctx.stroke(arrow);
ctx.restore();
Path2D objects can also combine other paths with addPath, which is useful for building complex shapes from simpler parts.
Common Pitfalls
The coordinate system starts at the top-left. Y increases downward, not upward. This catches people who know SVG or math with Cartesian grids.
Canvas dimensions vs CSS dimensions. Set the width and height attributes on the element to control the actual pixel buffer. CSS only controls display size. A mismatch between pixel buffer and display size makes the canvas look blurry on Retina/HiDPI screens.
Cross-origin images taint the canvas. If you draw an image from another domain without proper CORS headers, the canvas becomes “tainted” and you can no longer call getImageData, toDataURL, or toBlob on it. Set crossorigin="anonymous" on the image element and ensure the server sends Access-Control-Allow-Origin headers.
Paths accumulate. After fill() or stroke(), the current path is still there. Call beginPath() before starting a new shape, or subsequent fills will paint on top of previous ones.
See Also
- /guides/javascript-web-animations-api/ — combine Canvas with the Web Animations API for smooth, synchronized animation loops
- /guides/javascript-streams-api/ — process video frames or image data with readable streams for Canvas manipulation
- /guides/javascript-service-workers/ — cache Canvas-powered apps for offline use with service worker caching strategies
Written
- File: sites/jsguides/src/content/guides/javascript-canvas-api.md
- Words: ~1150
- Read time: 5 min
- Topics covered: canvas setup, getContext, rectangles, paths, arcs, curves, colors/gradients, text, images, transforms, pixel manipulation, Path2D, common gotchas
- Verified via: MDN Canvas API docs, research artifact
- Unverified items: none