Introduction to Canvas
The HTML <canvas> element gives you a blank, pixel-addressable rectangle that you paint with JavaScript. Unlike the rest of the DOM, where you describe elements and the browser draws them, canvas hands you a low-level drawing surface and a set of imperative commands: move here, stroke a line, fill a circle. That control is exactly why canvas powers charts, image editors, data visualizations, and full-blown browser games.
The canvas element
A canvas starts life as plain markup. On its own it renders nothing — it is a transparent, empty region until you draw into it from script.
<canvas id="scene" width="320" height="180"></canvas>
The element only becomes useful once you grab a rendering context from it. The context is the object that exposes every drawing method. For 2D graphics you request "2d"; the same element can instead hand back a "webgl" or "webgpu" context for hardware-accelerated 3D, but you choose exactly one per canvas.
const canvas = document.querySelector("#scene");
const ctx = canvas.getContext("2d");
console.log(ctx instanceof CanvasRenderingContext2D);
Output:
true
Once you hold ctx, the canvas API is just method and property calls on that object — fillRect, arc, fillStyle, and so on.
The coordinate system
Canvas uses a coordinate system whose origin (0, 0) sits in the top-left corner. The x-axis grows to the right and the y-axis grows downward — the opposite of the math convention you may remember from school. Every coordinate is measured in pixels relative to that origin.
(0,0) ───────────► x
│ ┌──────────────┐
│ │ │
│ │ canvas │
▼ │ │
y └──────────────┘
(width, height)
So ctx.fillRect(10, 20, 50, 30) draws a rectangle whose top-left corner is 10px from the left and 20px from the top, then extends 50px right and 30px down. Keeping the y-down rule in mind saves you from constantly flipping shapes upside down.
Drawing your first shape
Here is a complete, self-contained example. It grabs the 2D context and paints a filled rectangle and a stroked circle.
<!DOCTYPE html>
<html>
<head>
<style>
canvas { border: 1px solid #ccc; background: #0f172a; }
</style>
</head>
<body>
<canvas id="scene" width="320" height="180"></canvas>
<script>
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");
// A filled rectangle
ctx.fillStyle = "#38bdf8";
ctx.fillRect(20, 20, 120, 80);
// A stroked circle
ctx.beginPath();
ctx.arc(230, 90, 50, 0, Math.PI * 2);
ctx.strokeStyle = "#f472b6";
ctx.lineWidth = 4;
ctx.stroke();
</script>
</body>
</html>
Notice the pattern: set a style property (fillStyle, strokeStyle), then call a drawing method that consumes it. The canvas is a stateful surface — whatever you set stays in effect until you change it.
Element size vs. drawing size
This is the single most common canvas pitfall. A canvas has two completely different notions of size:
| Concept | Set with | Meaning |
|---|---|---|
| Drawing buffer | width / height attributes | Number of pixels you actually draw into |
| Display size | CSS width / height | How big the element appears on screen |
If you only set CSS, the drawing buffer stays at its default 300×150, and the browser stretches those 300×150 pixels to fill the styled box — producing a blurry, distorted image.
/* DON'T do this alone — it stretches a 300x150 buffer */
canvas { width: 600px; height: 300px; }
Always set the attributes to match the rendered size. For crisp output on high-DPI (Retina) displays, scale the buffer by devicePixelRatio and then scale the context back down:
<!DOCTYPE html>
<html>
<head><style>#hd { width: 300px; height: 150px; }</style></head>
<body>
<canvas id="hd"></canvas>
<script>
const canvas = document.getElementById("hd");
const dpr = window.devicePixelRatio || 1;
const cssWidth = 300;
const cssHeight = 150;
// Backing buffer is larger on high-DPI screens...
canvas.width = cssWidth * dpr;
canvas.height = cssHeight * dpr;
// ...but the displayed size stays the same.
canvas.style.width = `${cssWidth}px`;
canvas.style.height = `${cssHeight}px`;
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr); // now draw in CSS pixels
ctx.font = "20px sans-serif";
ctx.fillStyle = "#0f172a";
ctx.fillText("Crisp on any display", 20, 80);
</script>
</body>
</html>
Tip: Setting
canvas.widthorcanvas.height(even to the same value) clears the entire canvas and resets the context state. Treat a resize as a full repaint.
What canvas is good for
Canvas shines when you have many shapes, pixel-level effects, or frame-by-frame redraws — think games, particle systems, charts with thousands of points, or photo filters. Because it is a single bitmap, it has no per-shape DOM nodes, so it scales to huge numbers of drawn elements that would choke SVG.
The trade-off is that canvas is immediate-mode: once you draw a shape, the canvas forgets it ever existed. There is no “rectangle object” to move or click later — you redraw the whole scene yourself. For a handful of interactive, accessible vector shapes, SVG is often the better tool.
Best Practices
- Set the
width/heightattributes to the real pixel size; use CSS only for layout, never to resize the buffer. - Account for
devicePixelRatioso graphics stay sharp on high-DPI screens. - Cache
canvas.getContext("2d")once instead of calling it on every frame. - Remember canvas is stateful — set
fillStyle,lineWidth, etc. before each shape that needs them, and usesave()/restore()to isolate changes. - Clear with
ctx.clearRect(0, 0, canvas.width, canvas.height)before redrawing an animated scene. - Reach for SVG when you need a small number of interactive, accessible, or text-heavy vector elements instead.