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β
- HTML Code
- Browser Output
<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>
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 a button to drawPress the buttons, and each one runs a few lines of drawing code, shown underneath. The steps are always the same:
- Find the canvas element.
- Get its context with
getContext("2d"). The context is the object that has all the drawing methods. - Set a style (
fillStyle,strokeStyle,font, and so on). - Draw.
The Two Sizes of a Canvasβ
A canvas has two sizes, and mixing them up is the most common beginner problem:
| Size | Set by | Meaning |
|---|---|---|
| Bitmap size | The width and height attributes | How many pixels the drawing has (default 300 by 150) |
| Display size | CSS width and height | How 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β
| Task | Methods |
|---|---|
| Rectangles | fillRect(x, y, w, h), strokeRect(...), clearRect(...) |
| Paths | beginPath(), moveTo(x, y), lineTo(x, y), arc(x, y, r, start, end), closePath(), then fill() or stroke() |
| Curves | quadraticCurveTo(...), bezierCurveTo(...) |
| Text | fillText(text, x, y), strokeText(...), measureText(text) |
| Images | drawImage(image, x, y) |
| Colours | fillStyle, strokeStyle (colour names, hex, or gradients) |
| Lines | lineWidth, lineCap, lineJoin |
| Transforms | translate, rotate, scale, plus save() and restore() |
| Pixels | getImageData, putImageData |
A few tips:
- Always call
beginPath()before a new shape. If you forget, new lines join old ones, and everystroke()redraws all of them. save()andrestore()protect your settings. Callsave(), change styles or transforms, draw, and callrestore()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.
- HTML Code
- Browser Output
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);
Running at about 0 frames per second. Your screen's pixel ratio is 1.
On a high-resolution screen (pixel ratio above 1), turning βMatch screen sharpnessβ off makes the drawing blurry.
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.
- HTML Code
- Browser Output
<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>
Nothing selected yet.
The canvas is drawn at 300 by 150 but shown larger, so the page converts the mouse position back into canvas units. Without that step, clicks land in the wrong place.
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.
- Give it a name:
role="img"andaria-labelfor a drawing, oraria-labelledbypointing at a visible caption. - 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. - 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.
- 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. - Announce changes (a score, "Level 2", "Game over") in a
role="status"element. - Do not paint text-heavy interfaces on canvas. Menus, forms, and paragraphs belong in HTML, where they can be read, translated, searched, and focused.
- Respect motion preferences, give a pause control, and avoid flashing.
- Keep contrast high, and do not use colour alone to show meaning.
- Make targets large (at least 24 by 24 CSS pixels, ideally 44) for touch.
- 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".
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.
requestAnimationFramealready pauses in hidden tabs. For canvases scrolled off screen, useIntersectionObserver. - Do not make the canvas bigger than needed, especially on phones.
- For heavy work such as image processing, newer browsers offer
OffscreenCanvasand 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?β
| Canvas | SVG | |
|---|---|---|
| Made of | A bitmap you paint on | Elements in the page |
| After drawing | You only have pixels | Every shape is still an element |
| Sharp at any size | Only if you redraw at the right size | Yes |
| Accessibility | You supply everything | Mostly built in |
| Many moving objects | Fast | Slows down with thousands |
| Events on shapes | You work out which shape was hit | Built in (click on a shape) |
| Best for | Games, effects, image editing, particles | Icons, 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(), andgetImageData(), because the pixels could reveal private content. To read them, the image must be served with CORS headers, and requested withcrossorigin="anonymous"(orimg.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
fillTextcannot 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"andctx.textAligncarefully, and test mixed text. - Text does not wrap or resize itself. Longer translations may overflow, so measure with
measureTextand 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β
- Setting the size with CSS only, so the bitmap is stretched and blurry.
- Forgetting
beginPath(), so shapes join and redraw. - Using click coordinates directly, with no scaling.
- Not clearing the canvas between frames.
- Moving a fixed amount per frame, so speed changes with the screen.
- A canvas with no name or text alternative.
- Painting buttons, menus, or paragraphs that should be HTML.
- No keyboard alternative for pointer actions.
- Animation with no pause button, or ignoring reduced motion.
- Never stopping the animation loop.
- Blurry drawing on high-resolution screens.
- Drawing cross-origin images, then reading the pixels, and getting a security error.
Practice Timeβ
Try these in your editor:
- Create a
300 by 150canvas, and fill it with a coloured rectangle. - Draw a circle with
arc, and a triangle withmoveToandlineTo. - Write your name with
fillText, and measure its width withmeasureText. - Draw a shape with
save(), a changed style, andrestore(), and see that later shapes are unaffected. - Animate a square across the canvas with
requestAnimationFrameanddt. - Add a Pause button, and honour
prefers-reduced-motion. - Make the drawing sharp on a high-resolution screen with
devicePixelRatio. - Show the canvas larger with CSS, and make clicks land correctly.
- Add radio buttons that do the same job as clicking, with a
role="status"message. - 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, throughgetContext("2d").- Set the bitmap size with attributes, the display size with CSS, and use
devicePixelRatiofor sharpness. - Use
beginPath()for each shape, andsave()/restore()to protect styles. - Animate with
requestAnimationFrameand 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.