Skip to main content

HTML Canvas

If SVG is a box of shapes you can pick up and move, canvas is a blank painting that you paint on with code. You give it a size, you ask it to draw a circle here and a line there, and it paints the pixels. Once painted, the circle is not an object any more, just coloured dots. To move it, you wipe the area and paint again.

That makes canvas excellent for games, animation, visual effects, image editing, and anything with a very large number of moving parts. It also means you take on more work for accessibility, scaling, and clicks, which is the main subject of this lesson.


What You Will Learn​

  • Creating a canvas, and getting its 2D drawing context
  • Drawing rectangles, circles, lines, and text
  • Animating with requestAnimationFrame
  • Keeping drawings sharp on high-resolution screens
  • Handling clicks with the right coordinates
  • Making canvas accessible
  • Performance, security, and common mistakes

The Basics​

index.html
<canvas id="stage" width="300" height="150" role="img"
aria-label="A drawing area">
A drawing area. Shapes added with the buttons appear here.
</canvas>
script.js
const canvas = document.getElementById("stage");
const ctx = canvas.getContext("2d"); // the "pen" you draw with

ctx.fillStyle = "#2563eb";
ctx.fillRect(20, 20, 80, 40); // x, y, width, height

ctx.beginPath();
ctx.arc(180, 60, 25, 0, Math.PI * 2); // x, y, radius, start angle, end angle
ctx.fill();

ctx.font = "bold 18px sans-serif";
ctx.fillStyle = "#0f172a";
ctx.fillText("Hello canvas", 20, 120);

Press the buttons, and each one runs a few lines of drawing code, shown underneath. The steps are always the same:

  1. Find the canvas element.
  2. Get its context with getContext("2d"). The context is the object that has all the drawing methods.
  3. Set a style (fillStyle, strokeStyle, font, and so on).
  4. Draw.

The Two Sizes of a Canvas​

A canvas has two sizes, and mixing them up is the most common beginner problem:

SizeSet byMeaning
Bitmap sizeThe width and height attributesHow many pixels the drawing has (default 300 by 150)
Display sizeCSS width and heightHow big that bitmap is shown on the page

If the two differ, the browser stretches the bitmap, and your drawing becomes blurry or distorted. We deal with this again below, under sharpness and click coordinates.

Also note: setting width or height in script clears the canvas and resets all styles.

Coordinates​

As in SVG, the origin (0, 0) is the top-left corner. The x value grows to the right, and the y value grows downwards.

Drawing Methods​

TaskMethods
RectanglesfillRect(x, y, w, h), strokeRect(...), clearRect(...)
PathsbeginPath(), moveTo(x, y), lineTo(x, y), arc(x, y, r, start, end), closePath(), then fill() or stroke()
CurvesquadraticCurveTo(...), bezierCurveTo(...)
TextfillText(text, x, y), strokeText(...), measureText(text)
ImagesdrawImage(image, x, y)
ColoursfillStyle, strokeStyle (colour names, hex, or gradients)
LineslineWidth, lineCap, lineJoin
Transformstranslate, rotate, scale, plus save() and restore()
PixelsgetImageData, putImageData

A few tips:

  • Always call beginPath() before a new shape. If you forget, new lines join old ones, and every stroke() redraws all of them.
  • save() and restore() protect your settings. Call save(), change styles or transforms, draw, and call restore() to return to how things were.
  • Angles are in radians. A full circle is Math.PI * 2.
  • Text is painted, not typed. It does not wrap, cannot be selected, and screen readers cannot read it (more below).
// A small helper: draw a labelled dot, without leaking styles
function dot(ctx, x, y, color, label) {
ctx.save();
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, 8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#0f172a";
ctx.fillText(label, x + 12, y + 4);
ctx.restore();
}

Animation​

To animate, you erase and redraw many times a second. Use requestAnimationFrame, which calls your function just before the browser paints the next screen, usually 60 times a second, and pauses when the tab is hidden, saving battery.

script.js
const canvas = document.getElementById("stage");
const ctx = canvas.getContext("2d");

let x = 40, y = 40, vx = 140, vy = 100; // position, and speed in pixels per second
let last = performance.now();
let handle = 0;

function frame(now) {
const dt = Math.min(0.05, (now - last) / 1000); // seconds since the last frame
last = now;

x += vx * dt; // move by speed x time
y += vy * dt;
if (x < 14 || x > 286) vx = -vx; // bounce
if (y < 14 || y > 136) vy = -vy;

ctx.clearRect(0, 0, 300, 150); // erase
ctx.beginPath();
ctx.arc(x, y, 14, 0, Math.PI * 2); // redraw
ctx.fill();

handle = requestAnimationFrame(frame);
}

handle = requestAnimationFrame(frame);

// Stop it any time:
// cancelAnimationFrame(handle);

The most important idea is time-based movement. The ball moves by speed x dt, not "5 pixels per frame". That way it moves at the same real speed on a 60 Hz phone and a 144 Hz monitor.

Also note:

  • Provide a Pause button. Anything that moves for more than about 5 seconds should be pausable.
  • Respect prefers-reduced-motion. The demo starts paused if you have it switched on in your system.
  • Stop the loop (cancelAnimationFrame) when the canvas is not needed, such as when the visitor leaves the view or the page.

Sharp on Every Screen​

Many phones and laptops have high-resolution screens, where one CSS pixel is made of 2 or 3 device pixels. A canvas that has 300 bitmap pixels, shown at 300 CSS pixels, gets stretched to 600 or 900 device pixels, and looks blurry.

The fix is to make the bitmap bigger, and scale your drawing to match:

const ratio = window.devicePixelRatio || 1;

canvas.width = 300 * ratio; // the bitmap is bigger
canvas.height = 150 * ratio;
canvas.style.width = "300px"; // but shown at the same size
canvas.style.height = "150px";

ctx.setTransform(ratio, 0, 0, ratio, 0, 0); // so you can still draw in 300 by 150 units

Try the Match screen sharpness box in the demo above. On a standard-resolution screen you will not see a difference, but on a high-resolution one, switching it off makes the ball visibly soft. Do not forget to redo this when the canvas is resized, or when the visitor moves the window to a screen with a different ratio.

Very large canvases use lots of memory. Do not make a canvas bigger than you need.

Clicks and Touches: Get the Coordinates Right​

A pointer event gives you positions relative to the page or window. You need them in canvas units. When the displayed size differs from the bitmap size, you must scale:

canvas.addEventListener("click", (event) => {
const box = canvas.getBoundingClientRect();
const x = (event.clientX - box.left) * (canvas.width / box.width);
const y = (event.clientY - box.top) * (canvas.height / box.height);

// Is the point inside a circle?
const inside = Math.hypot(x - circle.x, y - circle.y) <= circle.r;
});

Try It: Click on a Circle, or Choose from a List​

Click a circle in the canvas. The page converts your click into canvas units, and tests which circle it hit. The canvas is shown at twice its bitmap size here, so skipping the conversion would put every click in the wrong place. Then use the list below, which does the same job for people who cannot click.

index.html
<canvas id="stage" width="300" height="150" role="img"
aria-label="Three circles: red, green and blue. Choose one with the list below."></canvas>

<fieldset>
<legend>The same choice, for keyboard users</legend>
<label><input type="radio" name="c" value="0"> Red</label>
<label><input type="radio" name="c" value="1"> Green</label>
<label><input type="radio" name="c" value="2"> Blue</label>
</fieldset>

<p id="status" role="status"></p>

Use pointer events (pointerdown, pointermove) rather than separate mouse and touch events, so one handler works for mouse, pen, and finger.

Accessibility​

A canvas is one picture to assistive technology. Everything you paint, including text, buttons, and charts, is invisible to a screen reader, cannot be searched, and cannot be reached by keyboard. You have to supply everything yourself.

  1. Give it a name: role="img" and aria-label for a drawing, or aria-labelledby pointing at a visible caption.
  2. Provide fallback content between the <canvas> tags. It is shown to browsers that cannot draw canvas, and exposed to some assistive technology. Do not rely on it alone.
  3. Offer a text alternative for anything that carries information. For a chart, put a data table or a text summary next to it. For a game, describe the state in text.
  4. Provide keyboard and screen reader alternatives for pointer actions, like the list in the demo above. Real <button>, <input>, and <select> elements next to the canvas are the most reliable way.
  5. Announce changes (a score, "Level 2", "Game over") in a role="status" element.
  6. Do not paint text-heavy interfaces on canvas. Menus, forms, and paragraphs belong in HTML, where they can be read, translated, searched, and focused.
  7. Respect motion preferences, give a pause control, and avoid flashing.
  8. Keep contrast high, and do not use colour alone to show meaning.
  9. Make targets large (at least 24 by 24 CSS pixels, ideally 44) for touch.
  10. Test with a keyboard and a screen reader, with the canvas as part of the whole page.

If the canvas is purely decorative, hide it from assistive technology with aria-hidden="true".

Build the page first, then add canvas

Make the information and controls work in plain HTML (a table, a list of buttons), then draw the canvas as an enhancement on top. That way the page stays usable if scripts or canvas fail.

Performance​

  • Draw only what changed, when you can. Clearing and redrawing a small area is cheaper than the whole canvas.
  • Avoid creating objects every frame (new arrays, strings, gradients). Reuse them.
  • Cache expensive drawings by painting them once onto a hidden second canvas, and then copy that with drawImage.
  • Stop when it is not visible. requestAnimationFrame already pauses in hidden tabs. For canvases scrolled off screen, use IntersectionObserver.
  • Do not make the canvas bigger than needed, especially on phones.
  • For heavy work such as image processing, newer browsers offer OffscreenCanvas and web workers. Check support before relying on them.
  • For thousands of 3D or shader-heavy objects, look at WebGL (getContext("webgl")), which uses the graphics card. It is a larger topic than this lesson.

Canvas or SVG?​

CanvasSVG
Made ofA bitmap you paint onElements in the page
After drawingYou only have pixelsEvery shape is still an element
Sharp at any sizeOnly if you redraw at the right sizeYes
AccessibilityYou supply everythingMostly built in
Many moving objectsFastSlows down with thousands
Events on shapesYou work out which shape was hitBuilt in (click on a shape)
Best forGames, effects, image editing, particlesIcons, logos, charts, diagrams

If you can do the job with SVG, do it with SVG. Choose canvas when you need speed, pixels, or effects.

Security and Privacy Notes​

  • Cross-origin images "taint" the canvas. If you draw an image from another site onto a canvas, the browser blocks toDataURL(), toBlob(), and getImageData(), because the pixels could reveal private content. To read them, the image must be served with CORS headers, and requested with crossorigin="anonymous" (or img.crossOrigin = "anonymous").
  • Canvas drawing can be used for fingerprinting. Scripts can draw hidden text and shapes, and read the tiny differences in how each device renders them, to identify a visitor. Do not use canvas for this. Be aware that some browsers add noise or ask permission for canvas reads.
  • Painted text is not HTML, so showing visitors' words with fillText cannot inject markup. Still validate any data that you draw.
  • Treat user-supplied images and files as untrusted, and check type and size. A giant image can freeze the page.
  • Do not put secrets in your drawing code. Anything in the page source is public.
  • Exporting canvas content (for example, a "download as PNG" button) gives visitors a copy of whatever was drawn, so do not draw private data on a shared page.

Global Audience Notes​

  • Text on canvas is not translatable by the browser, and cannot be read by many tools. Keep important text in HTML.
  • Fonts must be loaded before you draw them. Wait for document.fonts.ready (or load the font first), or the canvas will use a fallback font.
  • Choose fonts that cover your scripts, such as Arabic, Devanagari, or Chinese. Test that nothing shows as empty boxes.
  • Right-to-left text: set ctx.direction = "rtl" and ctx.textAlign carefully, and test mixed text.
  • Text does not wrap or resize itself. Longer translations may overflow, so measure with measureText and adjust.
  • Be kind to slow devices. Many visitors use older, low-power phones. Test at a low frame rate, offer a simpler or paused version, and keep the bitmap small.
  • Do not rely on colour meanings that differ between cultures, and add labels or shapes as well.

Common Mistakes to Avoid​

  1. Setting the size with CSS only, so the bitmap is stretched and blurry.
  2. Forgetting beginPath(), so shapes join and redraw.
  3. Using click coordinates directly, with no scaling.
  4. Not clearing the canvas between frames.
  5. Moving a fixed amount per frame, so speed changes with the screen.
  6. A canvas with no name or text alternative.
  7. Painting buttons, menus, or paragraphs that should be HTML.
  8. No keyboard alternative for pointer actions.
  9. Animation with no pause button, or ignoring reduced motion.
  10. Never stopping the animation loop.
  11. Blurry drawing on high-resolution screens.
  12. Drawing cross-origin images, then reading the pixels, and getting a security error.

Practice Time​

Try these in your editor:

  1. Create a 300 by 150 canvas, and fill it with a coloured rectangle.
  2. Draw a circle with arc, and a triangle with moveTo and lineTo.
  3. Write your name with fillText, and measure its width with measureText.
  4. Draw a shape with save(), a changed style, and restore(), and see that later shapes are unaffected.
  5. Animate a square across the canvas with requestAnimationFrame and dt.
  6. Add a Pause button, and honour prefers-reduced-motion.
  7. Make the drawing sharp on a high-resolution screen with devicePixelRatio.
  8. Show the canvas larger with CSS, and make clicks land correctly.
  9. Add radio buttons that do the same job as clicking, with a role="status" message.
  10. Bonus: draw a bar chart, and put the same numbers in a table underneath.

Quick Recap​

Click a question to check your answer.

1. What is the difference between the canvas width attribute and the CSS width?

The attribute sets the bitmap size (how many pixels you can draw on). CSS sets the displayed size. If they differ, the bitmap is stretched, which causes blur and wrong click positions.

2. Why use requestAnimationFrame instead of setInterval?

It runs just before each screen repaint, matches the display's refresh rate, and pauses in hidden tabs. Combined with time-based movement, the animation runs at the same speed on any screen.

3. How do you keep canvas drawings sharp on high-resolution screens?

Multiply the bitmap size by window.devicePixelRatio, keep the CSS size the same, and scale the context so you can still draw in CSS units.

4. How do you make canvas accessible?

Give it a name, add fallback content and a text alternative (such as a data table), provide real keyboard-friendly controls and status messages next to it, respect reduced motion, and keep text-heavy interfaces in HTML.

5. What does a "tainted" canvas mean?

Drawing a cross-origin image without CORS permission marks the canvas as tainted, and the browser blocks reading its pixels (toDataURL, getImageData) to protect private content.

6. When should you choose SVG over canvas?

When the graphic has a manageable number of shapes, needs to stay sharp at any size, or needs built-in accessibility and events: icons, logos, charts, and diagrams. Choose canvas for games, effects, and very large numbers of moving objects.

Key Takeaways​

  • <canvas> is a bitmap you paint with JavaScript, through getContext("2d").
  • Set the bitmap size with attributes, the display size with CSS, and use devicePixelRatio for sharpness.
  • Use beginPath() for each shape, and save()/restore() to protect styles.
  • Animate with requestAnimationFrame and time-based movement, with a pause button and reduced-motion support.
  • Convert pointer positions into canvas units before testing what was hit.
  • Canvas is not accessible by default: add a name, fallback content, text alternatives, and real controls.
  • Keep text-heavy UI in HTML, and use SVG when it can do the job.
  • Cross-origin images taint the canvas, and canvas can be used for fingerprinting, so be careful with both.

Next, show people where something is with Maps, or return to the Embedded Content overview.