Skip to content
JavaScript js canvas 4 min read

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:

ConceptSet withMeaning
Drawing bufferwidth / height attributesNumber of pixels you actually draw into
Display sizeCSS width / heightHow 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.width or canvas.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/height attributes to the real pixel size; use CSS only for layout, never to resize the buffer.
  • Account for devicePixelRatio so 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 use save()/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.
Last updated June 1, 2026
Was this helpful?