HTML <canvas> reference Jump to this section

Common attributes Jump to this section

Basic structure Jump to this section

The <canvas> element provides a drawable surface that JavaScript controls entirely. The element itself is just a blank rectangle; all content is drawn via the Canvas API using a rendering context obtained from getContext(). Without JavaScript, a canvas displays only its fallback content.

<canvas id="myCanvas" width="300" height="150">
    <!-- Fallback for unsupported browsers -->
    Your browser does not support canvas.
</canvas>

<script> const canvas = document.getElementById('myCanvas'); const ctx = canvas.getContext('2d'); ctx.fillStyle = '#3b82f6'; ctx.fillRect(20, 20, 60, 40); </script>

Canvas attributes Jump to this section

width Jump to this section

The width of the canvas drawing surface in CSS pixels. Defaults to 300 if not specified. This is distinct from CSS width, as it sets the resolution of the internal bitmap.

Always set width and height as HTML attributes (or via canvas.width in JS), not purely via CSS. Setting only CSS dimensions stretches the bitmap and causes blurry output.

<canvas width="400" height="150"></canvas>

<script> // Also settable in JS canvas.width = 400;

// Resizing clears the canvas // and resets all context state </script>

height Jump to this section

The height of the canvas drawing surface in CSS pixels. Defaults to 150 if not specified.

<canvas width="400" height="300"></canvas>

<script> // Also settable in JS canvas.height = 300; </script>

CSS sizing vs. attribute sizing Jump to this section

Setting width and height via CSS scales the canvas visually without changing the drawing resolution. This is useful for responsive layouts but can cause blurriness if the CSS size differs from the attribute size.

<!-- Blurry: bitmap is 300×150 but displayed at 600×300 -->
<canvas style="width: 600px; height: 300px;"></canvas>

<!-- Crisp: bitmap and display size match -->
<canvas width="600" height="300" style="width: 600px; height: 300px;"></canvas>

High-DPI (Retina) rendering Jump to this section

On high-DPI screens, multiply the canvas dimensions by devicePixelRatio and scale the context to keep output sharp.

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;

// Set display size via CSS
canvas.style.width  = '300px';
canvas.style.height = '150px';

// Set drawing buffer size
canvas.width  = 300 * dpr;
canvas.height = 150 * dpr;

// Scale context so coordinates still use CSS pixels
ctx.scale(dpr, dpr);

Rendering contexts Jump to this section

2d Jump to this section

The most widely used context. Provides a rich 2D drawing API including shapes, paths, text, images, gradients, and transformations.

const ctx = canvas.getContext('2d');
ctx.fillStyle = 'blue';
ctx.fillRect(10, 10, 100, 50);

webgl / webgl2 Jump to this section

Hardware-accelerated 3D rendering using the OpenGL ES API. Used for games, simulations, and data visualisations requiring GPU performance.

const gl = canvas.getContext('webgl');
// or
const gl2 = canvas.getContext('webgl2');

bitmaprenderer Jump to this section

Transfers an ImageBitmap to the canvas efficiently, useful when generating images off-screen.

const ctx = canvas.getContext('bitmaprenderer');
ctx.transferFromImageBitmap(bitmap);

getContext() options Jump to this section

The second argument to getContext() tunes context behaviour.

// 2D options
const ctx = canvas.getContext('2d', {
    alpha: false,          // No transparency (faster)
    willReadFrequently: true // Optimise for pixel reads
});

// WebGL options
const gl = canvas.getContext('webgl', {
    antialias: true,
    preserveDrawingBuffer: true
});

Fallback content Jump to this section

Any HTML placed between the <canvas> tags is shown only in browsers that do not support canvas (very rare today). Use it for meaningful alternatives rather than leaving it empty.

<canvas width="400" height="200">
    <!-- Image fallback -->
    <img src="chart-fallback.png" alt="Q1 sales chart">
</canvas>

<canvas width="300" height="150">
    <!-- Text fallback -->
    <p>Sales increased by 42% in Q1 2025.</p>
</canvas>

Accessibility attributes Jump to this section

role Jump to this section

<canvas> has no implicit ARIA semantics. Add an appropriate role to communicate its purpose.

<canvas role="img" aria-label="Bar chart of monthly sales"></canvas>
<canvas role="application" aria-label="Drawing tool"></canvas>

aria-label Jump to this section

Provides a text description for the canvas. Essential because screen readers cannot read drawn content.

<canvas width="400" height="200"
        role="img"
        aria-label="Pie chart: 60% JavaScript, 25% Python, 15% Other">
</canvas>

aria-describedby Jump to this section

References a more detailed description elsewhere on the page.

<canvas role="img" aria-describedby="chart-desc"></canvas>
<p id="chart-desc" class="visually-hidden">
    A line chart showing traffic growing from 1,000 visits in January
    to 8,500 visits in June.
</p>

tabindex Jump to this section

Makes the canvas keyboard-focusable, which is needed for interactive canvases such as games or drawing tools.

<canvas tabindex="0" role="application" aria-label="Drawing canvas">
</canvas>

Common attributes Jump to this section

id Jump to this section

Unique identifier. Almost always needed since JavaScript must obtain the canvas by reference.

<canvas id="gameCanvas" width="800" height="600"></canvas>

<script>
  const canvas = document.getElementById('gameCanvas');
  const ctx = canvas.getContext('2d');
</script>

class Jump to this section

CSS class names for styling the element (border, shadow, positioning).

<canvas class="chart-canvas rounded shadow"></canvas>

style Jump to this section

Inline CSS for positioning. Remember that CSS sizing does not affect drawing resolution.

<canvas width="600" height="400"
        style="width: 100%; max-width: 600px; display: block; margin: 0 auto;">
</canvas>

data-* attributes Jump to this section

Store metadata alongside the canvas. Useful for chart configuration, game state, or tool settings.

<canvas data-chart-type="bar"
        data-dataset="monthly-sales"
        width="600" height="300">
</canvas>

<script>
  const canvas = document.querySelector('canvas');
  console.log(canvas.dataset.chartType);  // "bar"
  console.log(canvas.dataset.dataset);    // "monthly-sales"
</script>

Drawing API Jump to this section

All drawing is done through the 2D rendering context. Get it once with canvas.getContext('2d') and then call methods on it to draw shapes, text, images, and more.

Rectangles Jump to this section

The fastest shapes to draw, no path required.

const ctx = canvas.getContext('2d');

// Filled rectangle ctx.fillStyle = '#3b82f6'; ctx.fillRect(x, y, width, height);

// Outlined rectangle ctx.strokeStyle = '#10b981'; ctx.lineWidth = 3; ctx.strokeRect(x, y, width, height);

// Erase rectangle area ctx.clearRect(x, y, width, height);

Paths and lines Jump to this section

Paths define arbitrary shapes. Always begin with beginPath().

ctx.beginPath();
ctx.moveTo(10, 90);      // Start point
ctx.lineTo(60, 15);      // Draw to
ctx.lineTo(110, 75);
ctx.lineTo(160, 20);

ctx.strokeStyle = '#3b82f6'; ctx.lineWidth = 2.5; ctx.lineJoin = 'round'; // 'round' | 'bevel' | 'miter' ctx.lineCap = 'round'; // 'round' | 'butt' | 'square' ctx.stroke();

Closed shapes and fill Jump to this section

// Triangle
ctx.beginPath();
ctx.moveTo(10, 90);
ctx.lineTo(60, 10);
ctx.lineTo(110, 90);
ctx.closePath();    // Connects back to moveTo point

ctx.fillStyle = 'rgba(59, 130, 246, 0.7)'; ctx.fill();

ctx.strokeStyle = '#2563eb'; ctx.lineWidth = 2; ctx.stroke();

Circles and arcs Jump to this section

// arc(x, y, radius, startAngle, endAngle, anticlockwise)
// Angles are in radians. 0 = 3 o'clock.
// Full circle
ctx.beginPath();
ctx.arc(50, 50, 35, 0, Math.PI * 2);
ctx.fillStyle = '#3b82f6';
ctx.fill();
// Semicircle
ctx.beginPath();
ctx.arc(130, 50, 35, 0, Math.PI);
ctx.stroke();
// Pie slice
ctx.beginPath();
ctx.moveTo(175, 50);  // Center
ctx.arc(175, 50, 22, -Math.PI / 2, Math.PI * 0.6);
ctx.closePath();
ctx.fill();

Rounded rectangles Jump to this section

// Modern API (Chrome 99+, Firefox 112+)
ctx.beginPath();
ctx.roundRect(10, 10, 80, 50, 12); // uniform radius
ctx.fill();
// Per-corner radii [tl, tr, br, bl]
ctx.beginPath();
ctx.roundRect(110, 10, 80, 50, [4, 16, 4, 16]);
ctx.fill();
// Manual fallback using arcTo
function roundRect(ctx, x, y, w, h, r) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.arcTo(x + w, y,     x + w, y + h, r);
    ctx.arcTo(x + w, y + h, x,     y + h, r);
    ctx.arcTo(x,     y + h, x,     y,     r);
    ctx.arcTo(x,     y,     x + w, y,     r);
    ctx.closePath();
}

Bezier curves Jump to this section

// Cubic Bezier (2 control points)
ctx.beginPath();
ctx.moveTo(10, 80);
ctx.bezierCurveTo(
    40, 10,   // control point 1
    80, 10,   // control point 2
    100, 80   // end point
);
ctx.stroke();
// Quadratic Bezier (1 control point)
ctx.beginPath();
ctx.moveTo(110, 80);
ctx.quadraticCurveTo(
    155, 10,  // control point
    190, 80   // end point
);
ctx.stroke();

Text Jump to this section

ctx.font = 'bold 22px sans-serif';
ctx.fillStyle = '#1f2937';
ctx.textAlign = 'left'; // 'left'|'center'|'right'|'start'|'end'
ctx.textBaseline = 'alphabetic'; // 'top'|'middle'|'bottom'

// Filled text ctx.fillText('Hello Canvas', 10, 38);

// Outlined text ctx.strokeStyle = '#3b82f6'; ctx.lineWidth = 1; ctx.strokeText('Outlined', 10, 90);

// Truncate long text to maxWidth ctx.fillText('Long text here', 10, 60, 180);

Gradients Jump to this section

// Linear gradient
const lg = ctx.createLinearGradient(x0, y0, x1, y1);
lg.addColorStop(0, '#3b82f6');
lg.addColorStop(1, '#8b5cf6');
ctx.fillStyle = lg;
ctx.fillRect(0, 0, 100, 100);
// Radial gradient (inner circle → outer circle)
const rg = ctx.createRadialGradient(
    cx, cy, innerRadius,
    cx, cy, outerRadius
);
rg.addColorStop(0, '#fde68a');
rg.addColorStop(1, '#f59e0b');
ctx.fillStyle = rg;
ctx.fillRect(100, 0, 100, 100);

Images Jump to this section

const img = new Image();
img.src = 'photo.jpg';
img.addEventListener('load', () => {
    // Basic draw
    ctx.drawImage(img, x, y);
    // Scaled draw
    ctx.drawImage(img, x, y, width, height);
    // Cropped draw (source crop → destination)
    ctx.drawImage(
        img,
        sx, sy, sWidth, sHeight, // source rect
        dx, dy, dWidth, dHeight  // dest rect
    );
});
// Draw from another canvas or video element
ctx.drawImage(otherCanvas, 0, 0);
ctx.drawImage(videoEl, 0, 0, 320, 180);

Shadows Jump to this section

// Drop shadow
ctx.shadowColor   = 'rgba(0, 0, 0, 0.4)';
ctx.shadowBlur    = 10;
ctx.shadowOffsetX = 4;
ctx.shadowOffsetY = 4;
ctx.fillStyle = '#3b82f6';
ctx.fillRect(20, 15, 70, 50);

// Glow effect (no offset) ctx.shadowColor = 'rgba(16, 185, 129, 0.6)'; ctx.shadowBlur = 18; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0;

// Reset shadows ctx.shadowColor = 'transparent';

Transformations Jump to this section

// Always save/restore to isolate transforms
ctx.save();

// Translate origin ctx.translate(55, 50);

// Rotate around origin (radians) ctx.rotate(Math.PI / 6); // 30 degrees

// Scale ctx.scale(1.4, 0.7);

// Draw at transformed coordinates ctx.fillRect(-25, -20, 50, 40);

// Restore previous state ctx.restore();

Composite operations Jump to this section

// Set how new drawing blends with existing content
ctx.globalCompositeOperation = 'source-over'; // default
ctx.globalCompositeOperation = 'multiply';
ctx.globalCompositeOperation = 'screen';
ctx.globalCompositeOperation = 'overlay';
ctx.globalCompositeOperation = 'destination-out'; // erase

// Global transparency ctx.globalAlpha = 0.5; // 0 = transparent, 1 = opaque

// Always reset when done ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1;

Clipping paths Jump to this section

ctx.save();

// Define clip region as any path ctx.beginPath(); ctx.arc(60, 50, 40, 0, Math.PI * 2); ctx.clip();

// Everything drawn now is clipped to that circle ctx.fillStyle = gradient; ctx.fillRect(0, 0, 200, 100);

// Restore removes the clip ctx.restore();

Pixel manipulation Jump to this section

Read and write raw pixel data with getImageData and putImageData.

const ctx = canvas.getContext('2d');

// Read pixels
const imageData = ctx.getImageData(x, y, width, height);
const pixels = imageData.data; // Uint8ClampedArray [R,G,B,A, R,G,B,A, ...]

// Invert colours
for (let i = 0; i < pixels.length; i += 4) {
    pixels[i]     = 255 - pixels[i];     // R
    pixels[i + 1] = 255 - pixels[i + 1]; // G
    pixels[i + 2] = 255 - pixels[i + 2]; // B
    // pixels[i + 3] is alpha, leave it alone
}

// Write pixels back
ctx.putImageData(imageData, x, y);

// Convert a specific pixel coordinate to array index
function pixelIndex(x, y, width) {
    return (y * width + x) * 4;
}

JavaScript patterns Jump to this section

Canvas applications are built around a rendering loop, event handling, and state management. These patterns cover the most common real-world use cases.

Animation loop Jump to this section

The standard pattern for smooth canvas animation using requestAnimationFrame.

const canvas = document.getElementById('my-canvas');
const ctx = canvas.getContext('2d');
let animationId = null;
let x = 0;
function draw(timestamp) {
    // Clear the previous frame
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // Update state
    x = (x + 1) % canvas.width;
    // Draw current frame
    ctx.fillStyle = '#3b82f6';
    ctx.beginPath();
    ctx.arc(x, canvas.height / 2, 12, 0, Math.PI * 2);
    ctx.fill();
    // Schedule next frame
    animationId = requestAnimationFrame(draw);
}
function start() {
    if (!animationId) {
        animationId = requestAnimationFrame(draw);
    }
}
function stop() {
    cancelAnimationFrame(animationId);
    animationId = null;
}

Mouse drawing Jump to this section

Capture mouse events to implement freehand drawing.

const canvas = document.getElementById('draw-canvas');
const ctx = canvas.getContext('2d');
let painting = false;
function getPos(e) {
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width  / rect.width;
    const scaleY = canvas.height / rect.height;
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const clientY = e.touches ? e.touches[0].clientY : e.clientY;
    return {
        x: (clientX - rect.left) * scaleX,
        y: (clientY - rect.top)  * scaleY
    };
}
canvas.addEventListener('mousedown',  e => { painting = true; ctx.beginPath(); });
canvas.addEventListener('mousemove',  e => {
    if (!painting) return;
    const { x, y } = getPos(e);
    ctx.lineTo(x, y);
    ctx.strokeStyle = colorPicker.value;
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(x, y);
});
canvas.addEventListener('mouseup',   () => { painting = false; });
canvas.addEventListener('mouseleave',() => { painting = false; });

Hit detection (click on shapes) Jump to this section

Test whether a mouse click lands inside a drawn shape.

const canvas = document.getElementById('hit-canvas');
const ctx = canvas.getContext('2d');
// Define shapes with metadata
const shapes = [
    { type: 'rect', x: 20,  y: 20, w: 70, h: 60, color: '#3b82f6', label: 'Blue box' },
    { type: 'circle', cx: 150, cy: 50, r: 35, color: '#10b981', label: 'Green circle' }
];
function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    shapes.forEach(s => {
        ctx.fillStyle = s.color;
        if (s.type === 'rect') {
            ctx.fillRect(s.x, s.y, s.w, s.h);
        } else {
            ctx.beginPath();
            ctx.arc(s.cx, s.cy, s.r, 0, Math.PI * 2);
            ctx.fill();
        }
    });
}
function hitTest(x, y) {
    return shapes.findLast(s => {
        if (s.type === 'rect') {
            return x >= s.x && x <= s.x + s.w &&
                   y >= s.y && y <= s.y + s.h;
        }
        const dx = x - s.cx, dy = y - s.cy;
        return Math.sqrt(dx * dx + dy * dy) <= s.r;
    });
}
canvas.addEventListener('click', e => {
    const rect = canvas.getBoundingClientRect();
    const x = (e.clientX - rect.left) * (canvas.width  / rect.width);
    const y = (e.clientY - rect.top)  * (canvas.height / rect.height);
    const hit = hitTest(x, y);
    msg.textContent = hit ? `Clicked: ${hit.label}` : 'Clicked background';
});

Resize canvas to fill its container Jump to this section

Keep the canvas resolution in sync with its CSS container size.

const canvas = document.getElementById('my-canvas');
const ctx    = canvas.getContext('2d');

function resize() {
    const dpr = window.devicePixelRatio || 1;
    const rect = canvas.parentElement.getBoundingClientRect();

    canvas.width  = rect.width  * dpr;
    canvas.height = rect.height * dpr;

    canvas.style.width  = rect.width  + 'px';
    canvas.style.height = rect.height + 'px';

    ctx.scale(dpr, dpr);

    // Re-draw after resize
    draw();
}

const resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(canvas.parentElement);
resize(); // Initial

Export as image Jump to this section

Save canvas content as a PNG or JPEG file.

// Export as PNG (lossless)
function savePNG(canvas, filename = 'canvas.png') {
    const a = document.createElement('a');
    a.download = filename;
    a.href = canvas.toDataURL('image/png');
    a.click();
}
// Export as JPEG (quality 0–1)
function saveJPEG(canvas, quality = 0.9) {
    const a = document.createElement('a');
    a.download = 'canvas.jpg';
    a.href = canvas.toDataURL('image/jpeg', quality);
    a.click();
}
// Get data URL for use as an img src or upload
const dataURL = canvas.toDataURL('image/png');
document.querySelector('img').src = dataURL;
// Async blob (better for large canvases)
canvas.toBlob(blob => {
    const url = URL.createObjectURL(blob);
}, 'image/png');

Clear the canvas Jump to this section

Several ways to erase canvas content.

const ctx = canvas.getContext('2d');

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

// Clear a region
ctx.clearRect(50, 50, 100, 80);

// Fill with a background colour instead of clearing
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);

// Nuclear option: resetting width clears and resets all state
canvas.width = canvas.width;

State management with save/restore Jump to this section

Isolate style and transformation changes so they don't leak.

const ctx = canvas.getContext('2d');

// The context state stack stores:
// fillStyle, strokeStyle, lineWidth, font,
// globalAlpha, globalCompositeOperation,
// transformations, clipping paths, and more

ctx.fillStyle = 'black'; // base state

ctx.save();              // push state onto stack
ctx.fillStyle = 'red';
ctx.translate(100, 50);
ctx.rotate(0.5);
ctx.fillRect(0, 0, 40, 40); // red, rotated rect
ctx.restore();           // pop state

ctx.fillRect(0, 0, 40, 40); // back to black, no rotation

Off-screen canvas Jump to this section

Render to an off-screen canvas for performance, then composite onto the visible canvas.

// OffscreenCanvas (modern, works in Web Workers)
const offscreen = new OffscreenCanvas(800, 600);
const offCtx = offscreen.getContext('2d');

offCtx.fillStyle = '#3b82f6';
offCtx.fillRect(0, 0, 800, 600);

// Composite to visible canvas
const visibleCtx = canvas.getContext('2d');
const bitmap = offscreen.transferToImageBitmap();
visibleCtx.drawImage(bitmap, 0, 0);

// Classic off-screen canvas (all browsers)
const buffer = document.createElement('canvas');
buffer.width  = 800;
buffer.height = 600;
const bufCtx = buffer.getContext('2d');
// ... draw to bufCtx ...
visibleCtx.drawImage(buffer, 0, 0);

Measure text Jump to this section

Calculate text dimensions before drawing to centre or clip text.

const ctx = canvas.getContext('2d');
ctx.font = 'bold 24px sans-serif';

const metrics = ctx.measureText('Hello Canvas');
const textWidth  = metrics.width;
const textHeight = metrics.actualBoundingBoxAscent
                 + metrics.actualBoundingBoxDescent;

// Centre text horizontally
const x = (canvas.width - textWidth) / 2;

// Centre text vertically
const y = (canvas.height + textHeight) / 2;

ctx.fillText('Hello Canvas', x, y);

Detect supported context types Jump to this section

Check what the browser supports before using a particular context.

function supportsContext(canvas, type) {
    try {
        return !!canvas.getContext(type);
    } catch {
        return false;
    }
}

if (supportsContext(canvas, 'webgl2')) {
    // Use WebGL 2
} else if (supportsContext(canvas, 'webgl')) {
    // Fall back to WebGL 1
} else {
    // Fall back to 2D
    const ctx = canvas.getContext('2d');
}