Tools Of The Browser

Canvas API: Draw, Edit Pixels, and Export Images

The Canvas API gives a page a bitmap surface for drawing shapes, gradients, text, pixel edits, animations, and exports such as PNG or WebP.

11 min read

Canvas API illustration with shapes, gradients, and code for web graphics

The Canvas API gives a web page a bitmap surface. JavaScript can draw into that surface, read pixels back, animate frames, and export the result as an image file.

Canvas is not the DOM. Once you draw text, shapes, or an image, the result is pixels. That makes canvas strong for graphics and image processing, but weak for accessible documents and editable text.

The canvas element is the bitmap

A canvas starts with an element:

<canvas id="preview" width="640" height="360"></canvas>

The width and height attributes set the internal bitmap size. CSS width and height only change how that bitmap is displayed.

This distinction matters. If the element has a 300 x 150 internal bitmap but CSS stretches it to 1200 x 600, the result looks soft because the browser is scaling too few pixels.

Start drawing by requesting a context:

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

The returned CanvasRenderingContext2D holds the drawing methods and state.

Coordinates and device pixels

Canvas coordinates start at (0, 0) in the top-left corner. x moves right, and y moves down.

On high-density screens, 1 CSS pixel may map to more than 1 device pixel. A common setup is to scale the internal bitmap by devicePixelRatio while keeping the CSS display size unchanged:

const width = 640;
const height = 360;
const dpr = window.devicePixelRatio || 1;

canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;

ctx.scale(dpr, dpr);

After scaling the context, you can draw using normal CSS-pixel coordinates while the bitmap keeps sharper detail.

Shapes, paths, and text

Canvas can draw rectangles, paths, arcs, images, gradients, and text.

ctx.fillStyle = "#111827";
ctx.fillRect(0, 0, 640, 360);

ctx.strokeStyle = "#46c2a5";
ctx.lineWidth = 4;
ctx.strokeRect(40, 40, 240, 120);

ctx.font = "600 32px sans-serif";
ctx.fillStyle = "white";
ctx.fillText("Canvas", 64, 112);

Text drawn this way is not selectable, searchable, or reflowable. Use DOM text for page content. Use canvas text for labels inside generated images, diagrams, badges, and exports.

Colors, gradients, and patterns

fillStyle and strokeStyle accept any CSS color string, including #hex, rgb(), hsl(), and oklch(). They also accept gradient and pattern objects.

A linear gradient runs along a line; a radial gradient runs between two circles:

const linear = ctx.createLinearGradient(0, 0, 200, 0);
linear.addColorStop(0, "#46c2a5");
linear.addColorStop(1, "#0f0f0f");
ctx.fillStyle = linear;
ctx.fillRect(10, 10, 200, 100);

const radial = ctx.createRadialGradient(100, 100, 0, 100, 100, 80);
radial.addColorStop(0, "#46c2a5");
radial.addColorStop(1, "#0f0f0f");
ctx.fillStyle = radial;
ctx.fillRect(0, 0, 200, 200);

A pattern repeats an image as a texture:

const pattern = ctx.createPattern(textureImage, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 400, 400);

The repeat mode can be repeat, repeat-x, repeat-y, or no-repeat.

Transforms and the state stack

Canvas keeps its own transform applied to the coordinate space before drawing. Moving, rotating, or scaling the space is often simpler than recomputing every coordinate.

ctx.save();
ctx.translate(150, 150);
ctx.rotate(Math.PI / 4);
ctx.fillRect(-50, -50, 100, 100);
ctx.restore();

The common transforms are translate(x, y), rotate(radians), scale(x, y), the raw matrix transform(a, b, c, d, e, f), and resetTransform().

save() and restore() are the key to using them safely. save() pushes the full drawing state onto a stack: the transform plus fillStyle, lineWidth, globalAlpha, shadows, and more. restore() pops the last snapshot and reverts everything changed since. Wrapping a transformed draw in save() and restore() keeps it from leaking into the next shape.

Compositing, alpha, and shadows

Three context settings control how new drawing combines with what is already on the canvas. Each one stays active until you change it back.

globalAlpha sets opacity for everything drawn after it, from 0 to 1:

ctx.globalAlpha = 0.5;
ctx.fillRect(50, 50, 100, 100);
ctx.globalAlpha = 1;

globalCompositeOperation decides how new pixels blend with existing ones. The default source-over paints on top. destination-over paints behind. multiply, screen, lighten, and darken mix tones the way layer blend modes do in an image editor:

ctx.globalCompositeOperation = "multiply";
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 100, 100);
ctx.fillStyle = "blue";
ctx.fillRect(50, 50, 100, 100);
ctx.globalCompositeOperation = "source-over";

Shadow properties apply to fills and strokes during the same paint:

ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 4;
ctx.shadowOffsetY = 4;
ctx.fillRect(50, 50, 100, 100);

shadowColor sets color and opacity, shadowBlur softens the edge, and the two offsets displace it. Reset shadowBlur to 0 when the next shape should be sharp.

Images and resizing

Canvas can draw images from <img>, <video>, ImageBitmap, and other canvas sources.

ctx.drawImage(image, 0, 0, targetWidth, targetHeight);

That call is the core of many browser image tools. Decode an image, draw it at a new size, then export the canvas. The resize happens during drawing.

When downscaling, the smoothing settings control quality:

ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";

Turn smoothing off for pixel art or QR codes, where hard edges should stay hard.

If the source image comes from another origin without the right CORS headers, the canvas can become tainted. A tainted canvas may still display, but the browser blocks pixel reads and exports to prevent cross-origin data leaks. Request the image with crossOrigin = "anonymous" and serve it with Access-Control-Allow-Origin to keep exports working.

Pixel editing

Canvas can expose raw pixel data through getImageData(). Each pixel appears as 4 channel values: red, green, blue, and alpha.

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;

for (let i = 0; i < data.length; i += 4) {
  const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
  data[i] = avg;
  data[i + 1] = avg;
  data[i + 2] = avg;
}

ctx.putImageData(imageData, 0, 0);

That example converts pixels to grayscale. More complex filters use the same idea: read channels, change values, write the pixels back.

Pixel loops can be expensive on large images. A 4000 x 3000 image has 12 million pixels, so a filter touches 48 million channel values.

The filter property

For standard effects, ctx.filter applies CSS filter functions during a draw, without a manual pixel loop:

ctx.filter = "blur(2px) contrast(120%) saturate(150%)";
ctx.drawImage(image, 0, 0);
ctx.filter = "none";

Filters chain left to right and run on the GPU where available, which makes them fast enough for live previews. Reach for getImageData() when you need custom math the built-in functions do not cover, and ctx.filter for blur, brightness, contrast, grayscale, sepia, and hue-rotate.

Exporting PNG, JPEG, and WebP

Canvas can export the current bitmap with toBlob():

canvas.toBlob(blob => {
  const url = URL.createObjectURL(blob);
  download.href = url;
}, "image/webp", 0.82);

The MIME type asks the browser for a format such as image/png, image/jpeg, or image/webp. The quality value applies to lossy encoders such as JPEG and WebP. PNG export is lossless, so quality does not reduce it in the same way.

toDataURL() returns the same image as a Base64 string instead of a Blob. It is convenient for small images and inline src values, but a Blob is the better choice for downloads and large files because it avoids holding the whole image as a string in memory.

Canvas export can drop metadata such as EXIF. Keep the original file when metadata matters.

Animation

Canvas animation redraws the bitmap for each frame. The normal browser-timed loop is requestAnimationFrame(), which fires before the next repaint, stays in sync with the display, and pauses in hidden tabs:

function draw(time) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillRect(40 + Math.sin(time / 300) * 20, 40, 80, 80);
  requestAnimationFrame(draw);
}

requestAnimationFrame(draw);

Each frame clears the old pixels and draws the new state. To stop a loop, keep the id it returns and pass it to cancelAnimationFrame(). Canvas does not remember objects for you. If you need selection, hit testing, or layers, your code has to track those structures separately.

OffscreenCanvas and workers

OffscreenCanvas lets canvas work run away from the visible DOM. Combined with a Web Worker, it can keep heavy rendering or image processing off the main UI thread.

That pattern fits batch image compression, filters, previews, thumbnails, and generated exports. It does not remove CPU cost. It keeps the interface responsive while the work runs elsewhere.

Canvas is the right tool when pixels are the output

Use canvas when the output is an image, animation frame, chart bitmap, thumbnail, filter result, or downloadable graphic.

Use HTML and SVG when the content needs semantic structure, selectable text, layout, links, or accessibility. Canvas can draw almost anything, but once drawn, the browser sees pixels rather than meaningful elements.

Read More From Our Blog

Read the blog →

Explore Our Tools

Browse all tools