HTML Canvas Basics
1. Introduction
The <canvas> element provides a pixel-based drawing surface that you control entirely with JavaScript. Unlike SVG’s vector model, canvas renders bitmap graphics: you draw shapes, paths, text, and images programmatically using a 2D rendering context. Canvas is the foundation of HTML5 games, data visualisations, image editors, and real-time graphics. This lesson covers the 2D context API, drawing primitives, and best practices for accessible canvas content.
2. Concept
Canvas vs SVG
| Feature | Canvas | SVG |
|---|---|---|
| Rendering model | Pixel-based (raster) | Vector-based (scalable) |
| DOM elements | None โ single bitmap | Each shape is a DOM node |
| Interactivity | Manual hit-testing in JS | Native events on shapes |
| Performance | Excellent for many objects | Degrades with many nodes |
| Accessibility | Requires manual ARIA | Better native accessibility |
| Best for | Games, real-time data, image processing | Icons, logos, charts, maps |
<canvas> tags, and consider adding a visually-hidden data table or description for data visualisations.ctx.save() before changing transforms or styles, ctx.restore() after. This prevents style changes from one draw operation leaking into the next.width and height attributes on <canvas>. Without them, the canvas defaults to 300ร150 pixels. Setting dimensions via CSS scales the canvas visually but stretches the bitmap, causing blurry output. Always set the intrinsic pixel dimensions via HTML attributes.3. Basic Example
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Canvas Demo</title></head>
<body>
<!-- Always provide a text fallback for accessibility -->
<canvas
id="myCanvas"
width="500"
height="300"
aria-label="Abstract colour composition: overlapping blue rectangle, red circle, and green triangle"
role="img"
>
<p>Your browser does not support the HTML canvas element.</p>
</canvas>
<script>
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
// Background
ctx.fillStyle = '#f8fafc';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Blue rectangle
ctx.fillStyle = '#3b82f6';
ctx.fillRect(50, 50, 150, 100);
// Red circle
ctx.beginPath();
ctx.arc(300, 150, 70, 0, Math.PI * 2);
ctx.fillStyle = '#ef4444';
ctx.fill();
// Green triangle
ctx.beginPath();
ctx.moveTo(400, 250);
ctx.lineTo(480, 80);
ctx.lineTo(320, 80);
ctx.closePath();
ctx.fillStyle = '#22c55e';
ctx.fill();
// Text
ctx.fillStyle = '#1e293b';
ctx.font = 'bold 18px system-ui';
ctx.fillText('HTML Canvas Demo', 20, 30);
// Stroked rectangle
ctx.strokeStyle = '#7c3aed';
ctx.lineWidth = 3;
ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20);
</script>
</body>
</html>
4. How It Works
Step 1 โ Getting the Context
canvas.getContext('2d') returns a CanvasRenderingContext2D object. All drawing operations are methods and properties on this object. The coordinate system has (0,0) at the top-left, x increases right, y increases down.
Step 2 โ Rectangles
ctx.fillRect(x, y, width, height) draws a filled rectangle immediately. ctx.strokeRect() draws only the outline. ctx.clearRect() erases pixels โ useful for animation frames.
Step 3 โ Paths and Shapes
Complex shapes use the path API: ctx.beginPath() starts a new path, then you add segments with moveTo(), lineTo(), arc(), bezierCurveTo(), etc. ctx.closePath() connects back to the start. Finally ctx.fill() or ctx.stroke() renders it.
Step 4 โ State Management
ctx.save() pushes the current state (fillStyle, strokeStyle, transforms, clip region) onto a stack. ctx.restore() pops it back. This is essential when you need to temporarily change styles for one draw operation without affecting subsequent ones.
5. Real-World Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bar Chart</title>
</head>
<body>
<h1>Q1 Sales by Region</h1>
<canvas
id="chart"
width="600"
height="350"
role="img"
aria-label="Bar chart: North ยฃ16,250, South ยฃ13,000, East ยฃ9,750, West ยฃ11,400"
>
<p>Q1 Sales โ North: ยฃ16,250 | South: ยฃ13,000 | East: ยฃ9,750 | West: ยฃ11,400</p>
</canvas>
<script>
var canvas = document.getElementById('chart');
var ctx = canvas.getContext('2d');
var W = canvas.width, H = canvas.height;
var pad = { top: 40, right: 30, bottom: 60, left: 70 };
var data = [
{ label: 'North', value: 16250, colour: '#3b82f6' },
{ label: 'South', value: 13000, colour: '#22c55e' },
{ label: 'East', value: 9750, colour: '#f59e0b' },
{ label: 'West', value: 11400, colour: '#ef4444' },
];
var maxVal = 20000;
var chartH = H - pad.top - pad.bottom;
var chartW = W - pad.left - pad.right;
var barWidth = (chartW / data.length) * 0.6;
var barGap = (chartW / data.length) * 0.4;
// Background
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, W, H);
// Title
ctx.save();
ctx.fillStyle = '#1e293b';
ctx.font = 'bold 16px system-ui';
ctx.fillText('Q1 2025 Regional Sales (ยฃ)', pad.left, 24);
ctx.restore();
// Y-axis labels and gridlines
ctx.save();
ctx.strokeStyle = '#e2e8f0';
ctx.fillStyle = '#64748b';
ctx.font = '12px system-ui';
ctx.textAlign = 'right';
for (var i = 0; i <= 4; i++) {
var val = (maxVal / 4) * i;
var y = pad.top + chartH - (val / maxVal) * chartH;
ctx.fillText('ยฃ' + (val / 1000) + 'k', pad.left - 8, y + 4);
ctx.beginPath();
ctx.moveTo(pad.left, y);
ctx.lineTo(pad.left + chartW, y);
ctx.stroke();
}
ctx.restore();
// Bars and labels
data.forEach(function(d, i) {
var x = pad.left + i * (chartW / data.length) + barGap / 2;
var barH = (d.value / maxVal) * chartH;
var y = pad.top + chartH - barH;
ctx.fillStyle = d.colour;
ctx.fillRect(x, y, barWidth, barH);
ctx.save();
ctx.fillStyle = '#1e293b';
ctx.font = '13px system-ui';
ctx.textAlign = 'center';
ctx.fillText(d.label, x + barWidth / 2, H - pad.bottom + 20);
ctx.fillText('ยฃ' + (d.value / 1000).toFixed(1) + 'k', x + barWidth / 2, y - 6);
ctx.restore();
});
// Axes
ctx.save();
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(pad.left, pad.top);
ctx.lineTo(pad.left, pad.top + chartH);
ctx.lineTo(pad.left + chartW, pad.top + chartH);
ctx.stroke();
ctx.restore();
</script>
</body>
</html>
6. Common Mistakes
❌ Setting canvas dimensions via CSS only โ blurry output
<canvas style="width:600px;height:300px"></canvas>
✓ Always set pixel dimensions via HTML attributes; use CSS only for display scaling
<canvas width="600" height="300" style="max-width:100%"></canvas>
❌ No accessibility fallback or ARIA label
<canvas id="chart"></canvas>
✓ Always include role, aria-label, and text fallback inside canvas tags
<canvas id="chart" width="600" height="350" role="img" aria-label="Bar chart showing Q1 sales">
<p>Q1 sales data: North ยฃ16,250, South ยฃ13,000</p>
</canvas>
7. Try It Yourself
8. Quick Reference
| Method / Property | Purpose | Example |
|---|---|---|
getContext('2d') |
Get 2D rendering context | var ctx = canvas.getContext(‘2d’) |
ctx.fillRect(x,y,w,h) |
Draw filled rectangle | ctx.fillRect(10, 10, 100, 50) |
ctx.strokeRect(x,y,w,h) |
Draw rectangle outline | ctx.strokeRect(10, 10, 100, 50) |
ctx.beginPath() |
Start a new path | Before arc/lineTo/moveTo |
ctx.arc(x,y,r,start,end) |
Draw arc or circle | ctx.arc(50,50,40,0,Math.PI*2) |
ctx.fill() / ctx.stroke() |
Render the current path | After beginPath + shape commands |
ctx.save() / ctx.restore() |
Save/restore canvas state | Wrap style changes that should not persist |