Concept
<canvas> is a single DOM element exposing an immediate-mode, pixel-based drawing API via JavaScript. "Immediate mode" is the key mental model: there's no retained scene graph of shapes you can later query, style, or attach event listeners to, you issue drawing commands that paint pixels onto a bitmap, and the moment you draw a circle, the browser has already forgotten it was a circle. Everything you see is just pixels; if you want to move, redraw, or hit-test a "shape," your own code has to track that, not the browser.
This is the fundamental contrast with SVG (a retained-mode, DOM-based vector format, every shape is a real, queryable, styleable, event-listenable DOM node) and with regular DOM/CSS. Canvas trades that structure away for raw pixel-level performance, drawing thousands of particles or manipulating individual pixels is something Canvas handles far better than thousands of DOM nodes ever could.
Basic 2D drawing
<canvas id="c" width="400" height="300"></canvas>const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#4f46e5";
ctx.fillRect(20, 20, 100, 80);
ctx.strokeStyle = "#111827";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(200, 100, 50, 0, Math.PI * 2);
ctx.stroke();
ctx.font
Important: set width/height as HTML attributes (or via canvas.width = 400 in JS), not CSS. Setting size via CSS (style="width: 400px") scales the existing bitmap rather than changing its actual resolution, the result is blurry, stretched output, a very common first-time Canvas bug.
The animation loop
function draw(timestamp) {
ctx.clearRect(0, 0, canvas.width, canvas.height); // Canvas doesn't auto-clear between frames
ctx.fillRect(x, y, 50, 50);
x += velocity;
requestAnimationFrame(draw); // sync to display refresh rate, pauses in background tabs
}
requestAnimationFrame(draw);requestAnimationFrame (not setInterval) is the correct scheduling primitive for any visual animation, it's synced to the browser's actual repaint cycle (typically 60fps, but matches the display's real refresh rate, including 120Hz+ displays) and automatically pauses when the tab is backgrounded, which setInterval does not do, wasting CPU/battery on hidden tabs.
Canvas never clears itself between draws, every frame, you're responsible for clearRecting (or otherwise painting over) the previous frame's content, or new drawing commands simply accumulate on top of old ones.
Pixel manipulation
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data; // Uint8ClampedArray: [r, g, b, a, r, g, b, a, ...]
for (let i = 0; i < pixels.length; i += 4) {
const gray = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
pixels[i] = pixels[i + 1] = pixels[i + 2] = gray; // grayscale filter
}
ctx.This is the capability SVG and DOM fundamentally cannot offer, direct, per-pixel read/write access to the rendered bitmap. It's how client-side image filters, color-picker eyedropper tools, and generative pixel effects are built.
Hit-testing (you must implement it yourself)
canvas.addEventListener("click", (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const dx = x - circleX;
const dy = y - circleY;
if (Math.sqrt(dx * dx + dy * dy) < circleRadius) {
console.log("Circle clicked!");
}
}Because Canvas has no retained shape objects, there's no circle.addEventListener('click', ...), you attach one listener to the whole <canvas> element and manually compute whether the click coordinates fall within whatever shape you conceptually drew there. For anything beyond a handful of simple shapes, this hand-rolled geometry gets complex fast, which is exactly the case where a library (Konva, Fabric.js, retained-mode abstractions built on top of Canvas) or switching to SVG becomes worth it.
2D context vs. WebGL context
canvas.getContext("2d"); // CPU/software-accelerated 2D drawing, this topic's focus
canvas.getContext("webgl"); // GPU-accelerated, for 3D or very high-volume 2D (particles, shaders)
canvas.getContext("webgl2"); // more capable WebGL, wider modern supportSame <canvas> element, different context APIs, WebGL is a much lower-level, GPU-programming API (shaders, buffers, matrices) suited for 3D graphics or performance-critical high-volume 2D (thousands of particles, custom shader effects), typically accessed via a library (Three.js, PixiJS) rather than raw WebGL directly in most application code.
Accessibility: the real cost of Canvas
Canvas content is, by default, completely invisible to screen readers and not part of the accessibility tree, it's just pixels, with no semantic information at all. This is Canvas's single biggest tradeoff versus SVG or DOM-based visuals.
<canvas id="chart" width="600" height="400" role="img" aria-label="Bar chart showing quarterly revenue growth from $2M to $5M">
<!-- Fallback content for browsers without canvas support, and a starting point for accessible descriptions -->
<p>Quarterly revenue: Q1 $2M, Q2 $3M, Q3 $4M, Q4 $5M.</p>
</canvas>For anything beyond decorative graphics, provide an aria-label/role="img" summary at minimum, and for data visualizations, seriously consider an accompanying visually-hidden data table or a text summary, a screen reader user gets literally zero information from an unlabeled <canvas> chart, where they'd get the underlying <table> data from an equivalent HTML table-based or well-labeled SVG chart.
Common Mistakes
1. Setting canvas size via CSS instead of the width/height attributes
/* Wrong, scales/blurs the existing bitmap */
canvas { width: 800px; height: 600px; }<!-- Right, sets the actual drawing resolution -->
<canvas width="800" height="600"></canvas>If you need the canvas to also be responsive/CSS-sized, set both: the width/height attributes for the actual pixel resolution, and CSS width/height to control the displayed size (ideally matching, or scaled deliberately for high-DPI via devicePixelRatio).
2. Forgetting clearRect between animation frames
Without it, every frame's drawing commands paint on top of the previous frame, motion smears into a solid trail rather than discrete moving shapes.
3. setInterval for animation instead of requestAnimationFrame
setInterval doesn't sync to the display refresh rate (causing stutter or wasted redraws), and critically doesn't pause in background tabs, burning CPU and battery for animation nobody's looking at.
4. Ignoring devicePixelRatio on high-DPI screens
const dpr = window.devicePixelRatio || 1;
canvas.width = displayWidth * dpr;
canvas.height = displayHeight * dpr;
canvas.style.width = displayWidth + "px";
canvas.style.height = displayHeight + "px";
ctx.scale(dpr, dpr);Without this adjustment, Canvas content looks visibly blurry on Retina/high-DPI displays, the canvas bitmap is rendered at CSS-pixel resolution, then upscaled by the display, exactly like a low-resolution image.
5. Zero accessibility consideration for data visualizations
Shipping a Canvas-rendered chart with no aria-label, no fallback text, and no accompanying data table makes that data completely inaccessible to screen reader users, not degraded, entirely absent.
6. Reaching for Canvas when SVG or DOM would genuinely be simpler
If you need a modest number of shapes that should be independently stylable, interactive, and accessible (icons, a diagram, a simple chart with tooltips per data point), SVG's retained DOM-node model is usually the better fit, Canvas's advantages (raw pixel access, huge shape counts) aren't relevant, and you pay for hand-rolled hit-testing and accessibility work SVG gives you for free.
Best Practices
- Set actual resolution via the
width/heightattributes, not CSS, and account fordevicePixelRatiofor crisp high-DPI rendering. - Use
requestAnimationFramefor any visual animation loop, neversetInterval. clearRect(or otherwise fully repaint) every frame unless a persistent trail effect is intentional.- Provide an accessible fallback,
aria-label/role="img"at minimum, an accompanying data table for genuine data visualizations. - once you need retained shape objects, hit-testing beyond trivial geometry, or GPU-accelerated rendering, don't hand-roll a scene graph from scratch for a production app.
Further Resources
- MDN, Canvas API
- MDN, Canvas tutorial
- MDN, Improving canvas performance
- web.dev, Canvas accessibility (historical Web Fundamentals guidance, still broadly applicable)
- Konva.js / Fabric.js, retained-mode Canvas libraries with built-in shape objects, events, hit-testing.
