HTML Canvas Essentials

Reviewed & published by Brayan K

The HTML <canvas> element is a blank, bitmap drawing surface that you control entirely from JavaScript through its 2D context, letting you draw shapes, text, images, and gradients pixel by pixel for games, charts, and animations.

Part of the free HTML & CSS course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

By the end you'll draw shapes, text, and gradients on a <canvas>, capture mouse input to draw freehand, and run a smooth animation loop with requestAnimationFrame.

What You'll Learn

Before you start: Canvas is driven entirely by JavaScript, so you'll want to be comfortable with variables, functions, and events first. If you need a refresher, revisit the JavaScript course.

💡 Real-World Analogy

Think of canvas as a whiteboard with a single marker. You don't place objects you can move later — you give the browser instructions ("put the pen here, draw a circle, fill it green"), and it stains the board with pixels. Once a stroke is down it isn't a separate "thing" anymore; to change it you wipe the area and redraw. That's why animation means clear, then redraw the whole scene on every frame.

1. The drawing context

The <canvas> element is just a blank rectangle until you grab its 2D context — the object that holds every drawing method. You get it once and reuse it:

Coordinates start at (0, 0) in the top-left; x grows to the right and y grows downward. Canvas is great for games, charts, and effects; for icons and logos prefer SVG (next lesson) because it stays sharp at any zoom.

Canvas vs SVG — pick the right tool

FeatureCanvasSVG
TypePixel-based (bitmap)Vector-based (DOM nodes)
ScalingGets blurry on zoomInfinitely sharp
Best forGames, visualisations, effectsIcons, logos, diagrams
InteractivityManual hit-testingNative DOM events per element
PerformanceBetter with many objectsSlows with 100s of nodes

2. Shapes, fills, strokes, text & gradients

Read this fully-commented example, then run it. Notice three patterns you'll use constantly: fill vs stroke (inside colour vs outline), paths (beginPath → arc/lineTo → fill/stroke), and a gradient built from colour stops.

<!DOCTYPE html>
<html>
<head>
<style>
  body { font-family: system-ui, sans-serif; padding: 24px; }
  /* Set the drawing size with width/height ATTRIBUTES below, not CSS */
  canvas { border: 2px solid #ddd; border-radius: 8px; background: #fafafa; display: block; }
</style>
</head>
<body>
  <h1>Shapes, Fills & Strokes</h1>
  <!-- width/height here set the real pixel resolution of the bitmap -->
  <canvas id="art" width="500" height="300">
    Your browser does not support canvas.  <!-- fallback text for screen readers -->
  </canvas>

  <script>
    // 1) Grab the canvas and ask it for a 2D drawing context.
    const canvas = document.getElementById('art');
    const ctx = canvas.getContext('2d');     // ctx = the "pen" you draw with

    // 2) A FILLED rectangle: fillStyle sets the colour, fillRect(x, y, w, h) paints it.
    ctx.fillStyle = '#1976D2';               // colour applies to every fill after this
    ctx.fillRect(20, 20, 120, 80);           // top-left at (20,20), 120 wide, 80 tall

    // 3) A STROKED (outline-only) rectangle. stroke = the border, fill = the inside.
    ctx.strokeStyle = '#E91E63';             // outline colour
    ctx.lineWidth = 4;                       // outline thickness in pixels
    ctx.strokeRect(170, 20, 120, 80);        // outline only — no fill

    // 4) A circle uses a PATH: begin it, arc it, then fill/stroke it.
    ctx.beginPath();                         // start a fresh path (always do this first!)
    ctx.arc(380, 60, 45, 0, Math.PI * 2);    // centre (380,60), radius 45, full circle
    ctx.fillStyle = '#4CAF50';
    ctx.fill();                              // paint the inside green

    // 5) A triangle from three line segments. closePath() draws back to the start.
    ctx.beginPath();
    ctx.moveTo(60, 200);                     // move the pen WITHOUT drawing
    ctx.lineTo(140, 200);                    // draw a line to here
    ctx.lineTo(100, 130);                    // and to here
    ctx.closePath();                         // joins the last point back to (60,200)
    ctx.fillStyle = '#FF9800';
    ctx.fill();

    // 6) Text: set the font, then fillText(text, x, y) — y is the text BASELINE.
    ctx.font = 'bold 18px system-ui';
    ctx.fillStyle = '#333';
    ctx.fillText('Canvas is powerful!', 180, 170);

    // 7) A gradient: define colour stops, then use it as a fillStyle.
    const grad = ctx.createLinearGradient(20, 240, 480, 240);  // left -> right
    grad.addColorStop(0, '#F44336');         // 0 = start
    grad.addColorStop(0.5, '#FF9800');       // 0.5 = middle
    grad.addColorStop(1, '#4CAF50');         // 1 = end
    ctx.fillStyle = grad;
    ctx.fillRect(20, 240, 460, 40);          // a smooth red->orange->green bar

    // ✅ Expected: blue filled box, pink outlined box, green circle, orange
    //    triangle, dark text, and a rainbow gradient bar along the bottom.
  </script>
</body>
</html>
The page this code makes: Worked example: shapes, fills & gradients
What this code shows in a browser window 720 pixels wide.

Important: Set canvas size with HTML attributes (<canvas width="500" height="300">), not CSS. CSS only stretches the bitmap — the real resolution stays at the attribute values, so mismatched CSS sizes produce blurry output.

3. 🎯 Your turn — a traffic light

Time to practise fill and beginPath. Fill in the blanks marked ___ following the 👉 hints. Each lamp is its own path, so beginPath() before every circle.

<!DOCTYPE html>
<html>
<head><style>
  body { font-family: system-ui, sans-serif; padding: 24px; }
  canvas { border: 2px solid #ddd; border-radius: 8px; background: #fff; display: block; }
</style></head>
<body>
  <h1>🎯 YOUR TURN — Draw a Traffic Light</h1>
  <canvas id="light" width="160" height="320">Canvas not supported.</canvas>
  <script>
    const ctx = document.getElementById('light').getContext('2d');

    // The black backing box is done for you:
    ctx.fillStyle = '#222';
    ctx.fillRect(40, 20, 80, 280);

    // 1) RED lamp — fill a circle at centre (80, 80), radius 28.
    ctx.beginPath();
    ctx.arc(80, 80, 28, 0, Math.PI * 2);
    ctx.fillStyle = ___;        // 👉 set this to 'red'
    ctx.fill();                 // 👉 and call fill() to paint it (uncomment if needed)

    // 2) AMBER lamp — its own NEW path at (80, 160), radius 28.
    ctx.beginPath();            // 👉 always beginPath() before a new shape
    ctx.arc(80, 160, 28, 0, Math.PI * 2);
    ctx.fillStyle = 'orange';
    ___;                        // 👉 call ctx.fill() here

    // 3) GREEN lamp — finish this one entirely (path, arc, colour, fill).
    // 👉 beginPath, arc at (80, 240) radius 28, fillStyle 'limegreen', fill

    // ✅ Expected: three glowing lamps (red, amber, green) on a black box.
  </script>
</body>
</html>

4. Capturing mouse input

To draw freehand you turn mouse positions into a path. On mousedown you start a path, on mousemove you lineTo the new point and stroke(), and you stop on mouseup. getBoundingClientRect() converts the page coordinates into canvas-local ones.

<!DOCTYPE html>
<html>
<head>
<style>
  body { font-family: system-ui, sans-serif; padding: 24px; }
  canvas { border: 2px solid #ddd; border-radius: 8px; background: #fff; cursor: crosshair; display: block; margin-top: 12px; }
  .controls { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
  .controls button { padding: 8px 16px; border: 2px solid #1976D2; background: transparent; color: #1976D2; border-radius: 6px; cursor: pointer; font-weight: 600; }
  .controls button:hover { background: #E3F2FD; }
  .controls input[type="color"] { width: 40px; height: 32px; border: none; cursor: pointer; }
</style>
</head>
<body>
  <h1>Drawing Pad — Hold & Drag to Draw</h1>
  <div class="controls">
    <input type="color" id="color" value="#1976D2">    <!-- pen colour -->
    <button id="clear">Clear</button>
  </div>
  <canvas id="pad" width="500" height="300">Canvas not supported.</canvas>

  <script>
    const pad = document.getElementById('pad');
    const ctx = pad.getContext('2d');
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';          // rounded line ends look like a real pen
    ctx.lineJoin = 'round';
    let drawing = false;

    // Convert a mouse event into canvas-local x/y coordinates.
    function pos(e) {
      const r = pad.getBoundingClientRect();
      return { x: e.clientX - r.left, y: e.clientY - r.top };
    }

    // mousedown: start a fresh path at the click point.
    pad.addEventListener('mousedown', (e) => {
      drawing = true;
      const p = pos(e);
      ctx.beginPath();
      ctx.moveTo(p.x, p.y);
    });

    // mousemove: while the button is held, extend the line and stroke it.
    pad.addEventListener('mousemove', (e) => {
      if (!drawing) return;
      const p = pos(e);
      ctx.lineTo(p.x, p.y);
      ctx.stroke();                 // draws the segment we just added
    });

    // stop drawing on mouseup or when the pointer leaves the canvas.
    pad.addEventListener('mouseup', () => { drawing = false; });
    pad.addEventListener('mouseleave', () => { drawing = false; });

    // colour picker updates the pen colour live.
    document.getElementById('color').addEventListener('input', (e) => {
      ctx.strokeStyle = e.target.value;
    });

    // Clear wipes the whole bitmap back to transparent.
    document.getElementById('clear').addEventListener('click', () => {
      ctx.clearRect(0, 0, 500, 300);
    });

    // ✅ Expected: hold the mouse and drag to draw; change colour; Clear erases.
  </script>
  <!-- ✅ Expected result, measured in a real browser:
     canvas -> display: block
     .controls -> display: flex
     .controls button -> cursor: pointer
     .controls input[type="color"] -> cursor: pointer
  -->
</body>
</html>
The page this code makes: Worked example: interactive drawing pad
What this code shows in a browser window 720 pixels wide.

5. Animating with requestAnimationFrame

An animation is just a function that draws one frame and then asks to be called again. requestAnimationFrame(fn) runs fn right before the next repaint (~60 times/sec). Each frame you clear (or paint a faint layer for trails), update positions, redraw, then schedule the next frame.

<!DOCTYPE html>
<html>
<head>
<style>
  body { font-family: system-ui, sans-serif; padding: 24px; }
  canvas { border: 2px solid #ddd; border-radius: 8px; background: #1a1a2e; display: block; }
</style>
</head>
<body>
  <h1>Bouncing Balls — requestAnimationFrame</h1>
  <canvas id="anim" width="500" height="300">Canvas not supported.</canvas>

  <script>
    const ctx = document.getElementById('anim').getContext('2d');

    // Build a few balls, each with a position, velocity (dx/dy), radius, colour.
    const balls = Array.from({ length: 8 }, () => ({
      x: Math.random() * 460 + 20,
      y: Math.random() * 260 + 20,
      dx: (Math.random() - 0.5) * 4,         // horizontal speed (can be negative)
      dy: (Math.random() - 0.5) * 4,         // vertical speed
      r: Math.random() * 15 + 8,
      color: 'hsl(' + Math.random() * 360 + ', 70%, 60%)'
    }));

    function animate() {
      // Semi-transparent fill instead of clearRect = motion trails.
      ctx.fillStyle = 'rgba(26, 26, 46, 0.2)';
      ctx.fillRect(0, 0, 500, 300);

      balls.forEach(b => {
        b.x += b.dx;                          // move
        b.y += b.dy;
        // Bounce off the walls by flipping the velocity.
        if (b.x - b.r < 0 || b.x + b.r > 500) b.dx *= -1;
        if (b.y - b.r < 0 || b.y + b.r > 300) b.dy *= -1;

        ctx.beginPath();                      // new path per ball
        ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
        ctx.fillStyle = b.color;
        ctx.fill();
      });

      requestAnimationFrame(animate);         // ~60 frames per second
    }
    requestAnimationFrame(animate);

    // ✅ Expected: 8 coloured balls bounce around leaving soft trails.
    //    (To draw a photo instead, load an Image then ctx.drawImage(img, x, y).)
  </script>
  <!-- ✅ Expected result, measured in a real browser:
     body -> padding-top: 24px
     canvas -> display: block
  -->
</body>
</html>

6. 🎯 Your turn — a moving square

Build your own animation loop. Fill in the three blanks: the clear method, and the function name passed to requestAnimationFrame so the loop keeps going.

<!DOCTYPE html>
<html>
<head><style>
  body { font-family: system-ui, sans-serif; padding: 24px; }
  canvas { border: 2px solid #ddd; border-radius: 8px; background: #1a1a2e; display: block; }
</style></head>
<body>
  <h1>🎯 YOUR TURN — Slide a Square Across</h1>
  <canvas id="stage" width="500" height="160">Canvas not supported.</canvas>
  <script>
    const ctx = document.getElementById('stage').getContext('2d');
    let x = 0;                  // the square's left edge

    function frame() {
      // 1) CLEAR the whole canvas so the old square doesn't smear.
      ctx.___(0, 0, 500, 160);  // 👉 method is clearRect (clears a rectangle)

      // 2) Draw the square at its current x.
      ctx.fillStyle = '#64B5F6';
      ctx.fillRect(x, 60, 40, 40);

      // 3) Move it 2px right; wrap back to the left when it leaves the screen.
      x += 2;
      if (x > 500) x = -40;

      // 4) Ask the browser to run this function again before the next repaint.
      requestAnimationFrame(___);   // 👉 pass the function name: frame

      // ✅ Expected: a blue square glides smoothly left → right, forever.
    }
    requestAnimationFrame(frame);   // kick off the loop
  </script>
</body>
</html>

7. A real example — bar chart from data

Loops + rectangles + text alignment combine into a genuine data visualisation. Watch how textAlign = 'center' changes what the x coordinate means for the labels.

<!DOCTYPE html>
<html>
<head>
<style>
  body { font-family: system-ui, sans-serif; padding: 24px; }
  canvas { border: 2px solid #ddd; border-radius: 8px; background: #fff; display: block; }
</style>
</head>
<body>
  <h1>Bar Chart from Data</h1>
  <canvas id="chart" width="500" height="300">Canvas not supported.</canvas>

  <script>
    const ctx = document.getElementById('chart').getContext('2d');
    const data = [
      { label: 'Jan', value: 65 }, { label: 'Feb', value: 45 },
      { label: 'Mar', value: 80 }, { label: 'Apr', value: 55 },
      { label: 'May', value: 92 }, { label: 'Jun', value: 70 }
    ];
    const maxVal = Math.max(...data.map(d => d.value));   // tallest bar = 92
    const colors = ['#1976D2', '#4CAF50', '#FF9800', '#E91E63', '#9C27B0', '#00BCD4'];
    const barW = 50, gap = 20, startX = 60, baseline = 260;

    // Chart title.
    ctx.fillStyle = '#1565C0';
    ctx.font = 'bold 16px system-ui';
    ctx.textAlign = 'left';
    ctx.fillText('Monthly Sales', startX, 25);

    data.forEach((d, i) => {
      const x = startX + i * (barW + gap);
      const h = (d.value / maxVal) * (baseline - 50);     // scale to fit
      // The bar itself.
      ctx.fillStyle = colors[i];
      ctx.fillRect(x, baseline - h, barW, h);
      // The month label, centred under the bar.
      ctx.fillStyle = '#333';
      ctx.font = 'bold 12px system-ui';
      ctx.textAlign = 'center';                            // x is now the bar's centre
      ctx.fillText(d.label, x + barW / 2, baseline + 18);
      // The value above the bar.
      ctx.fillStyle = colors[i];
      ctx.fillText(d.value, x + barW / 2, baseline - h - 8);
    });

    // ✅ Expected: six coloured bars of different heights with labels and values.
  </script>
  <!-- ✅ Expected result, measured in a real browser:
     body -> padding-top: 24px
     canvas -> display: block
  -->
</body>
</html>
The page this code makes: Worked example: bar chart
What this code shows in a browser window 720 pixels wide.

8. 🎯 Mini-challenge — smiley face

No blanks this time — just a comment outline. Combine everything you've learned (paths, arcs, fills, strokes) to draw a smiley face. The trick for the smile: an arc from 0 to Math.PI draws the bottom half of a circle.

<!DOCTYPE html>
<html>
<head><style>
  body { font-family: system-ui, sans-serif; padding: 24px; }
  canvas { border: 2px solid #ddd; border-radius: 8px; background: #fff; display: block; }
</style></head>
<body>
  <h1>🎯 MINI-CHALLENGE — Smiley Face</h1>
  <canvas id="face" width="240" height="240">Canvas not supported.</canvas>
  <script>
    const ctx = document.getElementById('face').getContext('2d');

    // 🎯 MINI-CHALLENGE: draw a smiley face from scratch.
    // 1. Head: a yellow filled circle, centre (120,120), radius 90.
    //    (beginPath → arc → fillStyle 'gold' → fill, then stroke for an outline)
    // 2. Eyes: two small black filled circles near (90,95) and (150,95).
    //    Remember beginPath() before EACH circle.
    // 3. Smile: an arc for the mouth — arc(120, 130, 50, 0, Math.PI) then stroke().
    //    (0 to Math.PI draws the BOTTOM half = a smile)
    //
    // ✅ Expected: a yellow face with two eyes and a curved smile.

    // your code here
  </script>
  <!-- ✅ Expected result, measured in a real browser:
     body -> padding-top: 24px
     canvas -> display: block
  -->
</body>
</html>

When to reach for canvas

Common Errors & Fixes

📋 Quick Reference

Method / PropertyWhat it does
canvas.getContext('2d')Get the 2D drawing context (the "pen")
fillStyle / strokeStyleColour for fills / for outlines
fillRect(x,y,w,h)Draw a filled rectangle
strokeRect(x,y,w,h)Draw a rectangle outline
clearRect(x,y,w,h)Erase a rectangular area to transparent
beginPath()Start a new path (call before each shape)
arc(x,y,r,0,Math.PI*2)Add a circle (or arc) to the path
moveTo(x,y) / lineTo(x,y)Move pen without drawing / draw a line
fill() / stroke()Paint the current path's inside / outline
fillText(text,x,y)Draw text (y is the baseline)
createLinearGradient(...)Make a gradient for fillStyle/strokeStyle
drawImage(img,x,y)Draw a loaded image onto the canvas
requestAnimationFrame(fn)Run fn before the next repaint (~60fps)

🎉 Lesson Complete

Practice quiz

How do you get the 2D drawing context from a canvas element?

  • canvas.getContext('2d')
  • canvas.draw2d()
  • canvas.context('2d')
  • canvas.get2D()

Answer: canvas.getContext('2d'). canvas.getContext('2d') returns the 2D drawing context — your 'pen' for all drawing methods.

Where is the origin point (0, 0) on a canvas?

  • The center
  • The bottom-left
  • The top-left
  • The top-right

Answer: The top-left. Canvas coordinates start at (0,0) in the top-left; x grows right and y grows downward.

What is the correct way to set a canvas's drawing resolution?

  • With CSS width and height
  • With the width and height HTML attributes
  • With the resolution attribute
  • It is set automatically

Answer: With the width and height HTML attributes. Set size with the width/height HTML attributes; CSS only stretches the bitmap and causes blur.

What does fillRect(x, y, w, h) do?

  • Draws a rectangle outline only
  • Draws a filled rectangle
  • Clears a rectangle
  • Defines a path

Answer: Draws a filled rectangle. fillRect paints a filled rectangle using fillStyle; strokeRect draws only the outline.

Why should you call ctx.beginPath() before drawing a new shape with a path?

  • To clear the canvas
  • To set the fill color
  • So the new shape isn't joined to the previous path
  • To start the animation loop

Answer: So the new shape isn't joined to the previous path. Without beginPath() the path keeps accumulating, so every new arc/line joins the previous shape.

What is the difference between fill() and stroke()?

  • fill() paints the inside, stroke() paints the outline
  • fill() paints the outline, stroke() paints the inside
  • They are identical
  • Only fill() works on paths

Answer: fill() paints the inside, stroke() paints the outline. fill() paints the inside of a path using fillStyle; stroke() paints its outline using strokeStyle.

Which method draws a full circle as a path?

  • circle(x, y, r)
  • arc(x, y, r, 0, Math.PI * 2)
  • ellipse(x, y, r)
  • drawCircle(x, y, r)

Answer: arc(x, y, r, 0, Math.PI * 2). arc(x, y, radius, 0, Math.PI * 2) sweeps a full 360-degree circle.

Which method should you call each frame to schedule smooth ~60fps animation?

  • setInterval(fn, 16)
  • requestAnimationFrame(fn)
  • setTimeout(fn, 60)
  • ctx.animate(fn)

Answer: requestAnimationFrame(fn). requestAnimationFrame(fn) runs fn right before the next repaint, about 60 times per second.

How do you erase a rectangular area of the canvas back to transparent?

  • ctx.eraseRect(...)
  • ctx.clearRect(x, y, w, h)
  • ctx.deleteRect(...)
  • ctx.reset()

Answer: ctx.clearRect(x, y, w, h). clearRect(x, y, w, h) erases that rectangle to transparent — used to clear each animation frame.

Compared to canvas, what is SVG better suited for?

  • Per-pixel image filters
  • Games with many sprites
  • Crisp icons and logos that scale
  • Real-time particle effects

Answer: Crisp icons and logos that scale. SVG is vector-based, staying sharp at any zoom — ideal for icons, logos, and diagrams.

Continue this course

Frequently asked questions

What is the HTML <canvas> element used for?

Canvas is a pixel-based drawing surface you control with JavaScript. It is ideal for games, data visualisations, image editing, charts, signature pads, and real-time effects. For static icons, logos, and diagrams, SVG is usually a better fit because it scales without blurring and its parts are real DOM elements.

Why is my canvas drawing blurry or stretched?

You almost certainly set the size with CSS instead of the HTML width and height attributes. CSS only stretches the existing bitmap, so the resolution stays low and the pixels smear. Always size the canvas with attributes like <canvas width="500" height="300">, and only use CSS if you genuinely want to stretch the result.

What is the difference between fill and stroke?

Fill paints the inside of a shape using fillStyle, while stroke paints only its outline using strokeStyle and lineWidth. fillRect and strokeRect are shortcuts for rectangles; for paths you call ctx.fill() and/or ctx.stroke() after defining the path. You can do both on the same shape to get an outlined, filled shape.

Why do all my shapes connect into one weird blob?

You forgot beginPath(). The canvas keeps adding to the current path until you start a new one, so every arc and lineTo joins the previous shape. Call ctx.beginPath() before each new shape and the problem disappears.

How do animations work on canvas?

Use requestAnimationFrame(callback). The browser calls your function about 60 times per second, right before each repaint. Inside it you clear the canvas (or paint a semi-transparent layer for trails), redraw everything at its new position, then call requestAnimationFrame again to schedule the next frame.

Is canvas accessible to screen readers?

Not by default — canvas pixels are invisible to assistive tech. Put meaningful fallback text between the <canvas> tags, add an aria-label or role, and where possible provide the same information in plain HTML nearby. For purely decorative drawings, mark them so screen readers can skip them.

Links on this page