The <canvas> 2D context (CanvasRenderingContext2D) typed for TypeScript: sizing for HiDPI, state,
paths, styles, text, images, pixels, animation, hit testing, workers and export. For GPU work see
WebGL and WebGPU.
Support & typing
Feature
Status (MDN)
Notes
2D context, Path2D, toBlob()
Baseline widely available
everywhere since ~2015
roundRect(), createConicGradient()
Baseline widely available
Safari 16.4
OffscreenCanvas, rAF in workers
Baseline widely available
Baseline 2023, Safari 16.4
createImageBitmap()
Baseline widely available
full options since Safari 17.2
reset()
Baseline widely available
Safari 17.2
willReadFrequently
Baseline 2024
Safari 18
letterSpacing, wordSpacing
Chrome 99, Firefox 115, Safari 18.4
ctx.filter
Limited availability
Safari only behind a flag
fontKerning, textRendering
Limited availability
not in Safari
contextlost event, isContextLost()
Limited availability
not in Safari
Type
Where you meet it
CanvasRenderingContext2D
canvas.getContext("2d") returns it | null
OffscreenCanvasRenderingContext2D
offscreen.getContext("2d"); no canvas DOM features
CanvasRenderingContext2DSettings
second arg of getContext("2d", …)
CanvasImageSource
anything drawImage accepts
CanvasFillRule
"nonzero" | "evenodd"
GlobalCompositeOperation
globalCompositeOperation values
ImageData
RGBA bytes in a Uint8ClampedArray
// One helper type for code shared by main thread and workertype Ctx2D = | CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;function get2d(canvas: HTMLCanvasElement) { const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("2D canvas unsupported"); return ctx; // CanvasRenderingContext2D}
Setup & HiDPI
A canvas has two sizes: the bitmap (width/height attributes, default 300×150) and the CSS box.
When they differ the bitmap is stretched, so set the bitmap to CSS size × devicePixelRatio and scale
the context back to draw in CSS pixels.
Thing
Meaning
canvas.width, canvas.height
bitmap pixels; setting either clears the canvas and resets all state
canvas.clientWidth / CSS width
layout size in CSS px
window.devicePixelRatio
device px per CSS px (2 on Retina, fractional when zoomed)
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
draw in CSS px on a HiDPI bitmap
ctx.imageSmoothingEnabled = false
crisp pixel art when scaling images
getContext("2d", opts)
Default
Effect
alpha
true
false = opaque backdrop, faster compositing
willReadFrequently
false
CPU-backed canvas; fast getImageData, slow drawing
The full helper and a resize-aware version are in Recipes.
State & transforms
save() pushes the whole drawing state onto a stack, restore() pops it. State covers: the transform,
clip region, fillStyle, strokeStyle, line props, font, text props, globalAlpha,
globalCompositeOperation, shadows, filter and imageSmoothing*. The current path and the
pixels are not state.
function fitText( ctx: CanvasRenderingContext2D, text: string, maxWidth: number,): string { if (ctx.measureText(text).width <= maxWidth) return text; let s = text; while (s && ctx.measureText(s + "…").width > maxWidth) { s = s.slice(0, -1); } return s + "…";}
Web fonts are not waited for: a fillText before the font loads draws the fallback. Await
document.fonts.load("16px Inter") (or document.fonts.ready) before the first draw. Canvas text has no
wrapping; split on words and measure.
For Safari-safe effects, apply a CSS filter to the <canvas> element itself or process pixels.
Animation loop
requestAnimationFrame(cb) calls cb(time) once before the next repaint (usually the display rate:
60, 120 or 144 Hz). It pauses in hidden tabs. Scale motion by elapsed time, never by frame count.
let last = performance.now();let x = 0;let rafId = 0;function frame(now: DOMHighResTimeStamp) { // Clamp: a backgrounded tab returns a huge delta const dt = Math.min((now - last) / 1000, 0.1); last = now; x = (x + 120 * dt) % ctx.canvas.width; // 120 px/s ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.fillRect(x, 20, 20, 20); rafId = requestAnimationFrame(frame);}rafId = requestAnimationFrame(frame);// stop: cancelAnimationFrame(rafId);
Rule
Why
One rAF loop per page
several loops fight over the frame budget
Use the now argument
same timestamp for all callbacks in a frame
Clear with clearRect (after save/resetTransform if transformed)
OffscreenCanvas is a canvas without a DOM node. Two uses: an in-memory buffer on any thread, or a
visible canvas whose drawing is moved to a worker with transferControlToOffscreen(). See
Web Workers for the messaging side.
main.ts
const view = document.querySelector("canvas")!;const off = view.transferControlToOffscreen();const worker = new Worker( new URL("./render.worker.ts", import.meta.url), { type: "module" },);// OffscreenCanvas is transferable, not copyableworker.postMessage({ canvas: off }, [off]);
offscreen.transferToImageBitmap() hands a frame to another canvas
Worker types
add "lib": ["webworker"] in a separate tsconfig for worker files
Exporting
Call
Returns
Notes
canvas.toBlob(cb, type?, quality?)
void; cb(Blob | null)
async encode; preferred
canvas.toDataURL(type?, quality?)
string
sync, blocks, ~33% larger (base64)
offscreen.convertToBlob(opts?)
Promise<Blob>
worker friendly
canvas.captureStream(fps?)
MediaStream
record with MediaRecorder
type defaults to image/png; image/jpeg and image/webp take a quality of 0–1. An unsupported
type falls back to PNG, so check blob.type. A tainted canvas throws SecurityError.
function canvasToBlob( canvas: HTMLCanvasElement, type = "image/png", quality?: number,): Promise<Blob> { return new Promise((resolve, reject) => { canvas.toBlob( (b) => b ? resolve(b) : reject(new Error("encode failed")), type, quality, ); });}
Performance & pitfalls
Do
Why
Batch same-style draws into one path, one fill()
state changes and draw calls cost more than geometry
Pre-render static layers to an OffscreenCanvas
drawImage a cached bitmap per frame
Stack canvases (background / sprites / UI)
redraw only the layer that changed
Round coordinates for sprites
sub-pixel positions trigger smoothing
Avoid shadowBlur and filter in hot paths
both rasterise per draw
Use ImageBitmap over <img>
already decoded
Keep getImageData out of the loop
GPU to CPU readback stalls
Cap bitmap size
max area is browser-dependent (Safari iOS is strict); huge canvases fail silently
Move heavy work to a worker
OffscreenCanvas keeps input responsive
Pitfall
Symptom
Fix
Only CSS size set
blurry output
set bitmap size × DPR
Setting width each frame
state lost, flicker
resize only on change
Forgot beginPath()
old shapes re-stroked, slows over time
start every shape with it
arc angles in degrees
wrong shape
radians: deg * Math.PI / 180
Mouse coords vs bitmap coords
hits miss on HiDPI or scaled canvas
scale by canvas.width / rect.width
Text drawn before font loads
fallback font
await document.fonts.load(...)
ctx.filter in Safari
no effect
CSS filter or manual pixels
Accessibility
A canvas is one opaque image to assistive tech. Give it meaning and keep interaction reachable.
Technique
Use for
role="img" + aria-label
static chart or illustration with a short summary
Fallback content inside <canvas>…</canvas>
read by screen readers; can hold a data table or links
Real DOM controls over or beside the canvas
buttons, inputs, focusable items
Visually hidden live region (aria-live="polite")
announce state changes (score, selection)
tabindex="0" + key handlers
keyboard play for games and editors
prefers-reduced-motion
stop or slow animation
Honor prefers-color-scheme / contrast
canvas colors don't follow CSS automatically
<canvas id="sales" role="img" aria-label="Sales rose 40% from January to June"> <table>…monthly sales…</table></canvas>
Recipes
HiDPI-correct setup
Use for any canvas that should look sharp on Retina and zoomed screens.