JavaScript Canvas: Drawing Shapes, Images, and Animation

JavaScript Canvas gives you a way to draw pixels, shapes, text, and images directly in the browser. It is ideal for custom graphics, charts, simple games, visualizations, and effects that are hard to build with normal HTML elements alone.

Quick answer: Canvas is an HTML element that you draw into with JavaScript through a rendering context, usually 2d. You set the canvas size, get the context, and then call drawing methods like fillRect(), arc(), and drawImage().

Difficulty: Beginner

You'll understand this better if you know: basic JavaScript syntax, how the DOM works, and the difference between HTML elements and browser rendering APIs.

1. What Is Canvas?

Canvas is an HTML element used for immediate-mode drawing. Instead of creating separate DOM nodes for each shape, you instruct the browser to paint graphics into a bitmap area. JavaScript then updates that bitmap whenever you redraw.

Unlike regular HTML content, canvas does not automatically remember individual objects you drew. If you want to change something, you usually clear and redraw the scene.

2. Why Canvas Matters

Canvas matters because it lets you build graphics that are difficult or inefficient with ordinary HTML and CSS. It is especially useful when the UI needs frequent updates or when the drawing must be treated as one image-like surface.

Canvas is not always the right choice. If each graphic element needs its own DOM behavior, accessibility tree, or fine-grained CSS styling, standard HTML elements may be better.

3. Basic Syntax or Core Idea

The core canvas workflow is simple: create a canvas element, get its drawing context, and use that context to draw.

Minimal example

This example shows the smallest useful canvas setup. The important detail is that the drawing happens through the context object, not directly on the canvas element.

<canvas id="myCanvas" width="300" height="150"></canvas>

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

ctx.fillStyle = "#4f46e5";
ctx.fillRect(20, 20, 120, 60);

The canvas element defines the drawing area. The getContext("2d") call returns the object that provides drawing methods and drawing state such as fill color, stroke color, and line width.

4. Step-by-Step Examples

Example 1: Draw a rectangle

A filled rectangle is the easiest first drawing to make. It uses the current fill style and a position plus size.

const ctx = document.querySelector("canvas").getContext("2d");

ctx.fillStyle = "tomato";
ctx.fillRect(10, 10, 80, 50);

This draws a solid tomato-colored rectangle starting at (10, 10). The four numbers are x, y, width, and height.

Example 2: Draw a circle

Canvas does not have a direct circle method. You draw circles by creating an arc path and then filling or stroking it.

const ctx = document.querySelector("canvas").getContext("2d");

ctx.beginPath();
ctx.arc(100, 75, 30, 0, Math.PI * 2);
ctx.fillStyle = "seagreen";
ctx.fill();

Here, beginPath() starts a new shape path, and arc() defines a full circle. The final fill() paints it.

Example 3: Draw text

Canvas can render text, but it uses pixel drawing rather than HTML text layout. That means alignment and fonts are controlled through canvas properties.

const ctx = document.querySelector("canvas").getContext("2d");

ctx.font = "20px sans-serif";
ctx.fillStyle = "#111827";
ctx.fillText("Hello Canvas", 20, 40);

This writes text at the specified coordinates. The text uses the font and fill style that were active when fillText() was called.

Example 4: Draw an image

One of the most common canvas tasks is displaying an image and then manipulating it or layering drawings on top of it.

const ctx = document.querySelector("canvas").getContext("2d");
const img = new Image();

img.src = "photo.jpg";
img.addEventListener("load", () => {
  ctx.drawImage(img, 0, 0, 300, 200);
});

The image must load before it can be drawn. The load event ensures the browser has the bitmap ready.

Example 5: Clear and redraw

Canvas often works by redrawing the entire scene on each update. Clearing is a normal part of animation and interaction.

function render(ctx) {
  ctx.clearRect(0, 0, 300, 150);
  ctx.fillStyle = "#2563eb";
  ctx.fillRect(20, 20, 50, 50);
}

This pattern is the foundation of animations and moving graphics: erase the old frame, then draw the new one.

5. Practical Use Cases

6. Common Mistakes

Mistake 1: Forgetting to get the rendering context

Canvas drawing methods belong to the context object, not the canvas element itself. Beginners often try to call drawing methods directly on the element.

Problem: This causes errors such as fillRect is not a function because the canvas element does not implement drawing APIs.

const canvas = document.getElementById("game");

canvas.fillRect(10, 10, 50, 50);

Fix: Get the 2d context first, then call drawing methods on that object.

const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");

ctx.fillRect(10, 10, 50, 50);

The corrected version works because the context is the object that actually knows how to draw.

Mistake 2: Setting the canvas size with CSS only

Canvas has two sizes: its internal drawing buffer size and its displayed CSS size. If you change only CSS, the image can look blurry or stretched.

Problem: The canvas may appear visually larger, but the drawing still happens in the default internal size, which causes scaling and poor sharpness.

<canvas id="chart" class="wide"></canvas>

/* CSS changes only the display size */
.wide { width: 600px; height: 300px; }

Fix: Set the element's width and height attributes to match the intended drawing buffer, and use CSS only for presentation when needed.

<canvas id="chart" width="600" height="300"></canvas>

This works because the bitmap buffer now matches the space you plan to draw into.

Mistake 3: Drawing before an image finishes loading

Canvas can only draw image data after the browser has loaded it. If you try too early, nothing appears.

Problem: Calling drawImage() before the image loads can produce a blank result or an InvalidStateError in some situations.

const ctx = document.querySelector("canvas").getContext("2d");
const img = new Image();
img.src = "photo.jpg";

ctx.drawImage(img, 0, 0);

Fix: Wait for the load event before drawing the image.

const ctx = document.querySelector("canvas").getContext("2d");
const img = new Image();

img.addEventListener("load", () => {
  ctx.drawImage(img, 0, 0);
});

img.src = "photo.jpg";

The corrected version works because the image data exists before the draw call runs.

Mistake 4: Forgetting that canvas does not preserve objects for you

Canvas is not a retained scene graph. If you move something and only draw the new position, the old drawing remains unless you clear and redraw.

Problem: The canvas keeps previously drawn pixels, so old positions can remain visible and create visual trails.

let x = 20;

function drawFrame(ctx) {
  ctx.fillRect(x, 20, 20, 20);
  x += 5;
}

Fix: Clear the drawing area before drawing the updated frame.

let x = 20;

function drawFrame(ctx) {
  ctx.clearRect(0, 0, 300, 150);
  ctx.fillRect(x, 20, 20, 20);
  x += 5;
}

This version works because each frame starts from a clean drawing surface.

7. Best Practices

Practice 1: Keep drawing and state separate

It is easier to maintain canvas code when the values that describe your scene are stored separately from the code that renders it.

const state = {
  x: 40,
  y: 50,
  radius: 20
};

function render(ctx, state) {
  ctx.clearRect(0, 0, 300, 150);
  ctx.beginPath();
  ctx.arc(state.x, state.y, state.radius, 0, Math.PI * 2);
  ctx.fill();
}

This approach makes it much easier to update positions, animate objects, and debug your scene.

Practice 2: Use animation frames instead of timers

For smooth animation, the browser's frame scheduler is usually better than fixed-interval timers.

function animate() {
  // update state here
  // render the new frame here
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

This helps canvas animation stay in sync with the browser's refresh cycle and avoids unnecessary redraws when the tab is inactive.

Practice 3: Match CSS size and drawing buffer size intentionally

If the display size and internal pixel size are different, your drawing can look blurry. Set both deliberately so the result is crisp.

<canvas id="board" width="400" height="200"></canvas>

/* Keep CSS and attribute sizing aligned when possible */
canvas { width: 400px; height: 200px; }

When you want a sharp result, the internal buffer should match the size the user actually sees.

8. Limitations and Edge Cases

9. Practical Mini Project

Let's build a tiny progress indicator that fills a bar and writes the percentage as text. This shows how to combine shapes, text, and redraw logic in one canvas.

<canvas id="progressCanvas" width="320" height="120"></canvas>

const canvas = document.getElementById("progressCanvas");
const ctx = canvas.getContext("2d");

let progress = 0;

function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = "#e5e7eb";
  ctx.fillRect(20, 40, 280, 24);

  ctx.fillStyle = "#22c55e";
  ctx.fillRect(20, 40, 280 * progress / 100, 24);

  ctx.fillStyle = "#111827";
  ctx.font = "16px sans-serif";
  ctx.fillText(`Loading: ${progress}%`, 20, 25);
}

function tick() {
  progress += 1;
  if (progress > 100) {
    progress = 0;
  }
  render();
  requestAnimationFrame(tick);
}

render();
requestAnimationFrame(tick);

This mini project shows the typical canvas loop: keep state in JavaScript, clear the surface, redraw the scene, and repeat with requestAnimationFrame().

10. Key Points

11. Practice Exercise

Create a canvas that draws three shapes: a blue square, a yellow circle, and a black text label. Make sure the shapes do not overlap in a confusing way.

Expected output: A simple drawing with three clearly separated items.

Hint: Use fillRect() for the square, arc() plus fill() for the circle, and fillText() for the label.

Solution:

<canvas id="exerciseCanvas" width="300" height="150"></canvas>

const canvas = document.getElementById("exerciseCanvas");
const ctx = canvas.getContext("2d");

// Blue square
ctx.fillStyle = "royalblue";
ctx.fillRect(20, 20, 40, 40);

// Yellow circle
ctx.beginPath();
ctx.arc(150, 50, 20, 0, Math.PI * 2);
ctx.fillStyle = "gold";
ctx.fill();

// Text label
ctx.fillStyle = "#111827";
ctx.font = "16px sans-serif";
ctx.fillText("Canvas practice", 20, 110);

This solution works because each shape is drawn in a controlled position with its own drawing call, and the label uses normal text rendering on the same surface.

12. Final Summary

JavaScript Canvas is a browser API for drawing graphics directly into a bitmap surface. It is one of the best tools for custom visuals that change frequently, such as animations, games, charts, and interactive drawing tools.

To use it well, remember the basic pattern: create a canvas element, get the drawing context, draw with that context, and redraw whenever the scene changes. Most beginner problems come from using the wrong object, sizing the canvas incorrectly, or drawing before assets are loaded.

Once you are comfortable with the 2D context, a natural next step is to learn animation loops, image manipulation, and advanced canvas state such as transforms, clipping, and compositing.