Skip to content
JavaScript js canvas 4 min read

Setting Up the Canvas

Before you draw a single pixel you need to get the canvas configured correctly, and this is where most beginners trip up. A canvas has two independent sizes, behaves differently on Retina screens, and has to be explicitly cleared between frames. Getting these three things right up front means your graphics will be crisp, correctly proportioned, and flicker-free on every device.

Markup and the 2D context

Every canvas project starts with a <canvas> element and a call to getContext("2d"), which returns the object that holds every drawing method. Cache that context once — there is no benefit to requesting it repeatedly, and calling getContext again just hands you the same object.

<canvas id="board" width="320" height="180"></canvas>
const canvas = document.querySelector("#board");
const ctx = canvas.getContext("2d");

console.log(canvas.width, canvas.height);
console.log(ctx.constructor.name);

Output:

320 180
CanvasRenderingContext2D

The two sizes of a canvas

This is the single most important concept in canvas setup. A canvas has a drawing buffer (the actual grid of pixels you paint into) and a display size (how large the element appears in the layout). They are controlled by completely different things.

SizeSet withControls
Drawing bufferwidth / height attributes (or canvas.width / canvas.height in JS)The number of pixels you draw into
Display sizeCSS width / heightHow big the element looks on screen

If you set only CSS, the buffer stays at its default 300×150 and the browser stretches those pixels to fill the styled box, producing a blurry result. The buffer should match the rendered size in real pixels.

/* WRONG when used alone — stretches a 300x150 buffer to 640x360 */
canvas {
  width: 640px;
  height: 360px;
}

Gotcha: Assigning canvas.width or canvas.height — even to the value it already holds — wipes the entire canvas and resets the context state (transforms, fillStyle, etc.). Treat any resize as a full repaint.

Handling high-DPI (retina) displays

On a Retina or other high-DPI screen, one CSS pixel maps to several physical pixels. The ratio is exposed as window.devicePixelRatio — typically 2 or 3 on modern phones and laptops. If your buffer is sized in CSS pixels, the browser upscales it and everything looks soft.

The fix is to size the buffer in physical pixels (CSS size × devicePixelRatio), pin the display size with CSS, then scale the context so your drawing code can keep working in convenient CSS-pixel coordinates.

function setupCanvas(canvas, cssWidth, cssHeight) {
  const dpr = window.devicePixelRatio || 1;

  // Backing buffer in physical pixels — sharp on any display.
  canvas.width = cssWidth * dpr;
  canvas.height = cssHeight * dpr;

  // Keep the on-screen size fixed in CSS pixels.
  canvas.style.width = `${cssWidth}px`;
  canvas.style.height = `${cssHeight}px`;

  const ctx = canvas.getContext("2d");
  // One CSS unit now equals `dpr` device pixels.
  ctx.scale(dpr, dpr);
  return ctx;
}

After ctx.scale(dpr, dpr), you draw using CSS-pixel coordinates and the context multiplies them up for you. Here is the full, runnable version so you can compare it against an unscaled canvas on a high-DPI screen.

<!DOCTYPE html>
<html>
<head>
  <style>
    canvas { border: 1px solid #ccc; background: #0f172a; display: block; }
  </style>
</head>
<body>
  <canvas id="hd"></canvas>
  <script>
    const canvas = document.getElementById("hd");
    const dpr = window.devicePixelRatio || 1;
    const cssWidth = 360;
    const cssHeight = 160;

    canvas.width = cssWidth * dpr;
    canvas.height = cssHeight * dpr;
    canvas.style.width = `${cssWidth}px`;
    canvas.style.height = `${cssHeight}px`;

    const ctx = canvas.getContext("2d");
    ctx.scale(dpr, dpr);

    // Drawn in CSS pixels, rendered at full device resolution.
    ctx.fillStyle = "#38bdf8";
    ctx.font = "28px sans-serif";
    ctx.fillText(`devicePixelRatio = ${dpr}`, 24, 60);

    ctx.strokeStyle = "#f472b6";
    ctx.lineWidth = 2;
    ctx.strokeRect(24, 90, 200, 48);
  </script>
</body>
</html>

Clearing the canvas

Canvas is immediate-mode: once drawn, a shape is just pixels with no memory of itself. For static art that is fine, but for animation or any repaint you must erase the old frame first. clearRect(x, y, width, height) resets a rectangular region to fully transparent.

// Erase the whole drawing surface before the next frame.
ctx.clearRect(0, 0, canvas.width, canvas.height);

Tip: When you have scaled the context with ctx.scale(dpr, dpr), clearRect(0, 0, canvas.width, canvas.height) still works because canvas.width is in device pixels and the transform applies to both. To clear in CSS pixels instead, use clearRect(0, 0, cssWidth, cssHeight).

The example below clears and redraws every frame, so the moving square leaves no trail. Comment out the clearRect line to see why clearing matters.

<!DOCTYPE html>
<html>
<head>
  <style>
    canvas { border: 1px solid #ccc; background: #0f172a; display: block; }
  </style>
</head>
<body>
  <canvas id="anim" width="360" height="160"></canvas>
  <script>
    const canvas = document.getElementById("anim");
    const ctx = canvas.getContext("2d");
    let x = 0;

    function frame() {
      // Remove this line and the square smears across the canvas.
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      ctx.fillStyle = "#34d399";
      ctx.fillRect(x, 60, 40, 40);

      x = (x + 2) % (canvas.width + 40);
      requestAnimationFrame(frame);
    }

    requestAnimationFrame(frame);
  </script>
</body>
</html>

To wipe to a solid colour instead of transparency, follow clearRect with a full-canvas fillRect, or just fillRect over everything if the background is opaque.

Best Practices

  • Set the width/height attributes to the real pixel size; use CSS only to control the displayed size, never to resize the buffer.
  • Multiply the buffer by window.devicePixelRatio and ctx.scale(dpr, dpr) so output stays sharp on Retina and other high-DPI screens.
  • Cache the result of getContext("2d") once rather than calling it per frame.
  • Remember that assigning to canvas.width/canvas.height clears the canvas and resets all context state.
  • Call ctx.clearRect(0, 0, canvas.width, canvas.height) at the top of each animation frame to avoid trails.
  • Re-run your DPI setup on resize or zoom, since devicePixelRatio can change when a window moves between monitors.
Last updated June 1, 2026
Was this helpful?