The canvas Element

A blank bitmap you draw on with JavaScript. Powerful for graphics and games, and invisible to every reader who cannot see the screen.

Concept

canvas is a rectangle of pixels that JavaScript draws into. The markup declares the surface and nothing else - every line, shape and character is drawn by script through a drawing context.

Because the result is a bitmap, nothing inside a canvas exists in the DOM. There are no elements to select, no text to search, and nothing for a screen reader to read. That constraint decides where canvas belongs.

Syntax

<canvas id="chart" width="600" height="400">
  A bar chart of bus ridership from 2023 to 2026, rising from 1.2 to 2.1 million.
</canvas>

The content between the tags is fallback, shown when canvas is unavailable. It is also the only text available to assistive technology, so describe what the canvas shows rather than writing your browser does not support canvas.

The width and height trap

The width and height attributes set the drawing surface in pixels. CSS width and height set the display size. They are different things, and mismatching them stretches the bitmap.

<!-- 300 by 150 pixels of bitmap stretched across 600 by 400: blurry -->
<canvas style="width:600px; height:400px"></canvas>

<!-- correct -->
<canvas width="600" height="400"></canvas>

The default surface is 300 by 150, which is why an unstyled canvas is that size. On a high density screen the surface should be scaled up and then shrunk with CSS:

const canvas = document.getElementById("chart");
const ratio  = window.devicePixelRatio || 1;

canvas.width  = 600 * ratio;
canvas.height = 400 * ratio;
canvas.style.width  = "600px";
canvas.style.height = "400px";

const ctx = canvas.getContext("2d");
ctx.scale(ratio, ratio);

Drawing

const ctx = document.getElementById("chart").getContext("2d");

// filled rectangle
ctx.fillStyle = "#1e3a8a";
ctx.fillRect(20, 20, 120, 80);

// outlined rectangle
ctx.strokeStyle = "#dc2626";
ctx.lineWidth = 3;
ctx.strokeRect(160, 20, 120, 80);

// a path
ctx.beginPath();
ctx.moveTo(20, 140);
ctx.lineTo(120, 220);
ctx.lineTo(220, 140);
ctx.closePath();
ctx.stroke();

// a circle
ctx.beginPath();
ctx.arc(320, 180, 40, 0, Math.PI * 2);
ctx.fillStyle = "#16a34a";
ctx.fill();

// text
ctx.font = "16px system-ui, sans-serif";
ctx.fillStyle = "#0f172a";
ctx.fillText("Ridership by year", 20, 280);

// an image
const img = new Image();
img.onload = () => ctx.drawImage(img, 20, 300, 160, 90);
img.src = "/images/logo.png";

// clear everything
ctx.clearRect(0, 0, canvas.width, canvas.height);

The coordinate origin is the top left corner, x increases to the right and y increases downwards.

Animation

let x = 0;

function frame() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "#1e3a8a";
  ctx.fillRect(x, 100, 40, 40);
  x = (x + 2) % canvas.width;
  requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

Use requestAnimationFrame rather than a timer. It matches the display refresh rate and pauses when the tab is hidden, which saves battery.

canvas or SVG?

canvasSVG
ModelPixelsElements in the DOM
Scales without lossNoYes
Selectable textNoYes
AccessibleOnly through fallback textYes, with title and desc
Events on shapesHit testing by handBuilt in
Many thousands of objectsFastSlows down
Styled with CSSNoYes

The rule: SVG for anything a reader might need to read, select or interact with. Canvas for large numbers of moving pixels. A chart on a content page should be SVG. A particle simulation or a game should be canvas.

Accessibility

A canvas is opaque to assistive technology. Anything meaningful drawn in it must be provided again in text.

<figure>
  <canvas id="chart" width="600" height="400" role="img"
          aria-label="Bus ridership by year, rising from 1.2 million in 2023 to 2.1 million in 2026">
  </canvas>
  <figcaption>Bus ridership, 2023 to 2026</figcaption>
</figure>

<table class="visually-hidden">
  <caption>Bus ridership by year</caption>
  <tr><th scope="col">Year</th><th scope="col">Passengers</th></tr>
  <tr><td>2023</td><td>1,200,000</td></tr>
  <tr><td>2026</td><td>2,100,000</td></tr>
</table>

The hidden table is the honest solution: the data is available to any reader, in a form they can navigate.

Important rules

  • width and height attributes set the surface, CSS sets the display size.
  • Setting either attribute clears the canvas entirely.
  • Nothing drawn in a canvas is in the DOM.
  • Drawing an image from another origin taints the canvas, and toDataURL then throws.
  • Text in canvas cannot be selected, searched, translated or zoomed by the browser.
  • getContext("2d") for two dimensional drawing; WebGL contexts are a separate topic.

Common mistakes

  • Sizing with CSS only and getting a stretched 300 by 150 bitmap.
  • Ignoring device pixel ratio and shipping blurry output on modern screens.
  • Resetting the width in a resize handler and wiping the drawing.
  • Using canvas for a chart on a content page, making the data unreadable.
  • Providing no fallback text.
  • Animating with setInterval instead of requestAnimationFrame.

Best practices

  • Set the surface size with attributes and the display size with CSS.
  • Scale for device pixel ratio.
  • Provide the same information in accessible text or a hidden table.
  • Choose SVG unless you have a specific reason not to.
  • Redraw on resize, and remember the canvas is cleared when you do.
  • Respect prefers-reduced-motion for animation.

Practice

  1. Draw a rectangle, a circle and a line, then style the canvas with CSS only and observe the distortion.
  2. Scale a canvas for device pixel ratio and compare the text sharpness before and after.
  3. Build a simple bar chart in canvas, then rebuild it in SVG. Compare accessibility.
  4. Animate a shape with requestAnimationFrame and confirm it pauses when the tab is hidden.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All HTML notes →
HTML

The template Element

Markup that is parsed but not rendered, waiting to be cloned. The clean way to build repeated content in JavaScript without writing HTML as strings.

Read more
HTML

What HTML5 Actually Means

HTML5 is not a version you upgrade to. It is the name for a shift from a frozen specification to a living standard, and for the elements and APIs that...

Read more
HTML

SVG in HTML

Vector graphics that scale perfectly, weigh almost nothing and can be styled with CSS. Learn inline SVG, SVG as an image, and how to make one accessib...

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.