Drawing Canvas HTML CSS JS — Free Drawing Board

Drawing Canvas · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Pointer Events API: unified mouse, touch and pen input with setPointerCapture for uninterrupted strokes
Dual-canvas overlay: a separate preview canvas renders the rubber-band line without touching the artwork
Composite operations: the eraser uses destination-out globalCompositeOperation to remove pixels cleanly
Flood fill: an iterative stack-based getImageData/putImageData algorithm fills enclosed regions by color match
Undo stack: bounded history of toDataURL snapshots restored via drawImage
PNG export: download the artwork through a data-URL anchor with the download attribute
High-DPI coordinate mapping: getBoundingClientRect scaling keeps strokes under the cursor at any size
Resize persistence: artwork is snapshotted and repainted when the window changes size
Adjustable brush: live color picker and 2–40px size slider with round line caps and joins
Zero dependencies: pure HTML, CSS and Canvas 2D API with no external libraries

About this UI Snippet

How to Build a Drawing Canvas with HTML5 Canvas and JavaScript

Screenshot of the Drawing Canvas snippet rendered live

A drawing canvas is a paint-style sketch surface where the user draws freehand strokes with the mouse, finger or stylus, switches between tools like brush, eraser, line and fill bucket, picks colors and brush sizes, undoes mistakes and exports the result as a PNG. This walkthrough explains exactly how the component is built using the HTML5 Canvas 2D API, the Pointer Events API and a small set of pixel-level algorithms, with no external libraries.

The two-canvas architecture

The board uses two stacked <canvas> elements inside a relatively positioned wrapper. The first, mainCanvas, holds the committed artwork. The second, overlayCanvas, sits directly on top with pointer-events: none and is used only as a live preview surface for the line tool, so a rubber-band line can be redrawn every frame without disturbing the real drawing underneath. Both are absolutely positioned at top: 0; left: 0 and sized to 100% of the wrapper, while their internal bitmap resolution is set in JavaScript to match the CSS pixel size.

A critical detail is the resize handler. Setting canvas.width or canvas.height clears the bitmap, so before resizing we snapshot the current image with mainCanvas.toDataURL(), set the new dimensions, repaint the dark background, then load the snapshot into an Image and drawImage it back. This preserves artwork across window resizes. The 2D context is obtained with getContext('2d'); for components that read pixels frequently you would pass { willReadFrequently: true } to hint the browser to keep the buffer in system memory rather than the GPU.

Pointer Events and coordinate mapping

All input is handled through the Pointer Events API (pointerdown, pointermove, pointerup) rather than separate mouse and touch listeners, so a single code path covers mouse, touch and pen. On pointerdown we call mainCanvas.setPointerCapture(e.pointerId) so the element keeps receiving move and up events even when the pointer leaves the canvas mid-stroke — this prevents broken lines when you drag fast past the edge.

Because the canvas bitmap resolution can differ from its displayed CSS size, raw client coordinates must be scaled. The getPos helper reads getBoundingClientRect() and computes x = (e.clientX - rect.left) * (canvas.width / rect.width), mapping screen space into bitmap space so strokes land under the cursor exactly even on high-DPI displays or after resizing.

The tool state machine

A single tool string variable ('brush', 'eraser', 'line', 'fill') drives behavior. setTool updates this variable, toggles the .active class on the toolbar buttons and updates the status label. On each pointerdown the handler branches on tool:

For brush and eraser, we begin a path with ctx.beginPath() and ctx.moveTo(x, y), then on every pointermove we lineTo the new point and stroke(), building a continuous polyline. lineCap and lineJoin are set to round so segments blend into a smooth stroke. The eraser is simply a brush that sets ctx.globalCompositeOperation = 'destination-out', which makes drawing operations remove existing pixels (turning alpha to zero) instead of painting color over them. The brush resets the mode back to source-over.

The line tool and the overlay

The line tool demonstrates why the overlay exists. On pointerdown we record startX, startY. On pointermove we clear the overlay and draw a fresh preview line from the start point to the current point on overlayCanvas only. The user sees the line stretch and rotate in real time, but the main artwork is untouched. On pointerup we clear the overlay and commit the final line to mainCanvas. This is the classic two-layer rubber-band pattern used in vector editors.

The flood fill algorithm

The fill bucket is the most interesting piece. floodFill grabs the entire pixel buffer with ctx.getImageData(0, 0, w, h), giving a flat Uint8ClampedArray where every pixel occupies four bytes (R, G, B, A). It samples the target color at the clicked pixel using the index formula idx = (py * width + px) * 4. If the target color already equals the fill color, it returns early to avoid an infinite loop.

It then runs an iterative stack-based flood fill (an explicit stack instead of recursion to avoid blowing the call stack on large regions). Starting from the clicked pixel, it pops a coordinate, checks bounds, compares that pixel's four channels against the target color, and if they match, overwrites them with the fill color and pushes the four orthogonal neighbors. When the stack empties, the modified buffer is written back with ctx.putImageData(imgData, 0, 0). The fill color comes from hexToRgba, which slices the hex string and parseInt(..., 16) each channel.

Undo via snapshots

Undo is implemented as a bounded history array of data URLs. Before any destructive action (saveHistory) we push mainCanvas.toDataURL() and shift() off the oldest entry once we exceed MAX_HISTORY (20) to cap memory. Undo pop()s the latest snapshot, loads it into an Image, clears the canvas and drawImages it back. Storing full PNG snapshots is simple and robust; a more memory-efficient alternative would store ImageData and putImageData, trading higher RAM for instant restores with no decode step.

Exporting the drawing

Download uses the canvas-to-anchor pattern: create an <a> element, set a.download = 'drawing.png' and a.href = mainCanvas.toDataURL(), then call a.click() to trigger the browser save dialog. toDataURL() defaults to PNG which preserves transparency from the eraser. For very large canvases, canvas.toBlob() with URL.createObjectURL() avoids building a huge base64 string in memory.

Together these techniques — stacked canvases, pointer capture, composite operations, an explicit-stack flood fill and data-URL snapshots — produce a fully functional, dependency-free drawing board you can drop into any page.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of tracing the pixel algorithm by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why floodFill uses an explicit stack array instead of a recursive function, and how the getPos coordinate scaling by canvas.width divided by the bounding rect's width keeps strokes aligned under the cursor even when the canvas's CSS size differs from its bitmap resolution. The same assistant can help you optimize it, for instance checking whether storing full toDataURL PNG snapshots for undo is worth the memory cost compared to storing raw ImageData, especially as MAX_HISTORY grows. It's also useful for extending the tool: ask it to add a fill-tolerance threshold so flood fill doesn't leak through anti-aliased edges, support a rectangle or ellipse shape tool using the same overlay-canvas rubber-band pattern as the line tool, or add pressure-sensitive line width for stylus input. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a drawing canvas application in plain HTML, CSS, and JavaScript using the Canvas 2D API and the Pointer Events API — no drawing library.

Requirements:
- Two stacked, identically-sized canvas elements: a main canvas holding the committed artwork, and a second overlay canvas on top with pointer-events disabled, used only to preview an in-progress straight line without touching the main artwork until the user releases the pointer.
- Handle all input through pointerdown, pointermove, and pointerup (not separate mouse and touch listeners), calling setPointerCapture on pointerdown so a fast drag that leaves the canvas bounds mid-stroke still continues receiving move events instead of producing a broken line.
- Convert every pointer event's client coordinates into canvas bitmap coordinates by scaling through the ratio of the canvas's internal width/height to its displayed bounding-rect width/height, so strokes land exactly under the cursor regardless of how the canvas is sized by CSS or scaled for high-DPI screens.
- Implement a brush tool that begins a path on pointerdown and extends it with lineTo plus stroke on every pointermove, and an eraser tool that behaves identically except it sets the canvas context's globalCompositeOperation to destination-out so strokes remove existing pixels instead of painting over them.
- Implement a flood fill tool using getImageData to read the full pixel buffer, an iterative stack-based algorithm (explicit array used as a stack, not recursion) that compares each pixel's four RGBA channel values against the clicked pixel's original color and replaces matching connected pixels with the new fill color, then writes the modified buffer back with putImageData.
- Implement undo as a capped history array: before every destructive action, push a snapshot of the canvas taken with toDataURL, discarding the oldest snapshot once a maximum history length is exceeded, and restore the most recent snapshot by loading it into an Image element and redrawing it onto a cleared canvas.
- Preserve the artwork across window resizes by snapshotting the canvas before changing its width/height (which otherwise clears the bitmap) and redrawing the snapshot back after the resize, and implement a PNG export using toDataURL and a generated anchor element with a download attribute.

Step by step

How to Use

  1. 1
    Pick a toolClick brush, eraser, line or fill bucket in the toolbar. The active tool is highlighted and shown in the status bar.
  2. 2
    Set color and sizeChoose a color with the native color input and drag the size slider to set stroke width from 2 to 40 pixels.
  3. 3
    Draw on the canvasPress and drag to paint freehand. For the line tool, drag to preview a straight line, then release to commit it.
  4. 4
    Fill regionsSelect the fill bucket and click any enclosed area to flood it with the current color using a pixel-matching algorithm.
  5. 5
    Undo or clearUse Undo to step back through the last 20 snapshots, or Clear to reset the whole board to the background color.
  6. 6
    Export your artClick Save to download the canvas as a PNG via a generated data URL anchor.

Real-world uses

Common Use Cases

Freehand sketch pad
Let users doodle and annotate directly in the browser, pairing well with an image filter editor for post-processing.
Whiteboard mockups
Quick wireframing and diagramming where straight lines and fills matter more than precision vector tooling.
Signature capture
Collect handwritten signatures on forms using the brush tool and export the result as a PNG.
Drawing games
Power Pictionary-style guessing games or kids drawing apps with simple, fast tools.
Teaching Canvas APIs
A compact reference for learning getImageData, putImageData and composite operations alongside a pattern lock demo.
Markup and feedback
Annotate screenshots or images with arrows, circles and highlights before sharing.

Got questions?

Frequently Asked Questions

The overlay canvas previews the in-progress line tool every frame without redrawing or corrupting the committed artwork. When the pointer is released, the final line is drawn once onto the main canvas and the overlay is cleared.

It sets ctx.globalCompositeOperation to destination-out. In that mode, drawing operations subtract alpha from existing pixels instead of painting color, effectively erasing. The brush resets the mode back to source-over.

Each snapshot is a full PNG captured with toDataURL, which is simple and reliable. Restoring loads the image and redraws it. For lower memory you could store ImageData objects and use putImageData instead, at the cost of higher RAM usage.

The flood fill matches exact RGBA values. Anti-aliased stroke edges contain semi-transparent pixels that do not match the target color, so fills can bleed through soft boundaries. Drawing fully opaque outlines or adding a color tolerance threshold avoids this.

The pointer coordinates are scaled by canvas.width / boundingRect.width, mapping CSS pixels into the canvas bitmap resolution so the painted point always lands directly under the cursor.

Yes. The JSX, Vue, Angular, and Tailwind exports convert the markup automatically. In React, grab the canvas through a ref, set up the 2D context and pointer listeners in useEffect with cleanup, and keep tool, colour, and brush size in state read by the stroke handlers.