How to Create a Blank Drawing Canvas on a Website
Create a blank, responsive HTML canvas and add mouse, pen, and touch drawing with sharp high-DPI output.
Use the native HTML <canvas> element. Give it intrinsic width and height attributes, obtain a 2D context with getContext('2d'), and leave the context untouched. The result is a blank surface ready for drawing. When dimensions are omitted, browsers start with a 300 × 150 pixel canvas.
1. Minimal blank canvas
This complete page creates a blank 800 × 500 pixel drawing area with fallback text for browsers that do not support canvas.
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Blank drawing canvas</title>
<style>
#drawing-canvas {
display: block;
width: 800px;
height: 500px;
border: 1px solid #bbb;
background: white;
}
</style>
</head>
<body>
<label for='drawing-canvas'>Drawing area</label>
<canvas id='drawing-canvas' width='800' height='500'>
Your browser does not support canvas; use the drawing controls below instead.
</canvas>
<script>
const canvas = document.getElementById('drawing-canvas');
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('2D canvas is not supported');
// Do not draw here: the canvas remains blank.
</script>
</body>
</html>
The width and height attributes set the bitmap resolution. CSS controls how that bitmap is displayed. Setting only CSS dimensions stretches the bitmap and can make drawing look distorted (MDN canvas documentation).
2. Make the canvas drawable with mouse, pen, and touch
Pointer Events provide one input model for a mouse, stylus, or finger. Capture the pointer when it goes down so strokes continue even if the contact leaves the element briefly.
const canvas = document.getElementById('drawing-canvas');
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('2D canvas is not supported');
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = '#111';
let drawing = false;
function pointInCanvas(event) {
const rect = canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * (canvas.width / rect.width),
y: (event.clientY - rect.top) * (canvas.height / rect.height)
};
}
canvas.addEventListener('pointerdown', (event) => {
drawing = true;
canvas.setPointerCapture(event.pointerId);
const point = pointInCanvas(event);
ctx.beginPath();
ctx.moveTo(point.x, point.y);
});
canvas.addEventListener('pointermove', (event) => {
if (!drawing) return;
const point = pointInCanvas(event);
ctx.lineTo(point.x, point.y);
ctx.stroke();
});
function stopDrawing(event) {
drawing = false;
if (event && canvas.hasPointerCapture(event.pointerId)) {
canvas.releasePointerCapture(event.pointerId);
}
}
canvas.addEventListener('pointerup', stopDrawing);
canvas.addEventListener('pointercancel', stopDrawing);
For touch devices, add touch-action: none to the canvas CSS so the browser does not interpret a stroke as scrolling or a gesture:
#drawing-canvas {
touch-action: none;
}
The coordinate conversion accounts for a difference between the intrinsic bitmap size and the element’s CSS size. Without it, strokes land at the wrong position when the canvas is responsive.
3. Prevent blur on high-DPI screens
A responsive canvas should keep its intended CSS size while using a larger backing bitmap based on window.devicePixelRatio. Scale the drawing context to match.
const canvas = document.getElementById('drawing-canvas');
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('2D canvas is not supported');
function resizeCanvas() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
Keep the CSS dimensions in your stylesheet:
#drawing-canvas {
width: min(100%, 800px);
height: auto;
aspect-ratio: 8 / 5;
border: 1px solid #bbb;
touch-action: none;
}
Changing the canvas width or height clears its drawing buffer. If resizing must preserve strokes, store strokes as data and redraw them after resizing, or save the bitmap with getImageData() before replacing the dimensions. MDN describes the device-pixel-ratio approach in its pixel ratio guidance.
4. A complete responsive drawing example
This single file combines a blank start state, high-DPI sizing, pointer input, color selection, clear, and PNG download.
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Drawing canvas</title>
<style>
.toolbar { display: flex; gap: .5rem; margin-bottom: .5rem; }
#drawing-canvas {
display: block;
width: min(100%, 800px);
height: auto;
aspect-ratio: 8 / 5;
border: 1px solid #bbb;
background: #fff;
touch-action: none;
}
</style>
</head>
<body>
<div class='toolbar'>
<label>Color <input id='color' type='color' value='#111111'></label>
<button id='clear' type='button'>Clear</button>
<button id='save' type='button'>Save PNG</button>
</div>
<label for='drawing-canvas'>Drawing area</label>
<canvas id='drawing-canvas'>Canvas is unavailable. Use the controls or an alternative drawing form.</canvas>
<script>
const canvas = document.getElementById('drawing-canvas');
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('2D canvas is not supported');
const color = document.getElementById('color');
let drawing = false;
function resizeCanvas() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
}
function pointInCanvas(event) {
const rect = canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * (canvas.width / rect.width),
y: (event.clientY - rect.top) * (canvas.height / rect.height)
};
}
canvas.addEventListener('pointerdown', (event) => {
drawing = true;
canvas.setPointerCapture(event.pointerId);
const p = pointInCanvas(event);
ctx.strokeStyle = color.value;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
});
canvas.addEventListener('pointermove', (event) => {
if (!drawing) return;
const p = pointInCanvas(event);
ctx.lineTo(p.x, p.y);
ctx.stroke();
});
function stop(event) {
drawing = false;
if (event && canvas.hasPointerCapture(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
}
canvas.addEventListener('pointerup', stop);
canvas.addEventListener('pointercancel', stop);
document.getElementById('clear').addEventListener('click', () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
document.getElementById('save').addEventListener('click', () => {
const link = document.createElement('a');
link.download = 'drawing.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
</script>
</body>
</html>
5. Preserve strokes and implement undo
A bitmap-only editor cannot reliably reconstruct earlier states. For undo, retain a stroke model such as an array of points, color, and width. Clear the bitmap and redraw every remaining stroke after an undo or resize. This also avoids losing artwork when a responsive resize changes the backing dimensions.
const strokes = [];
let activeStroke = null;
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const stroke of strokes) {
ctx.strokeStyle = stroke.color;
ctx.lineWidth = stroke.width;
ctx.beginPath();
stroke.points.forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
ctx.stroke();
}
}
function undo() {
strokes.pop();
redraw();
}
6. Accessibility and fallback content
Canvas pixels are not semantic HTML. Put fallback text between the opening and closing tags, give the area a visible label, and provide keyboard-accessible HTML buttons for clear, undo, save, and color selection. If drawing is essential to a task, provide a non-canvas alternative such as a text field, structured form, or uploaded image workflow. See MDN’s basic canvas usage and accessibility guidance.
7. Choosing an implementation
| Decision | Use this when | Trade-off |
|---|---|---|
| 2D canvas | Freehand drawing, signatures, annotations, and simple editors | Simple API; you manage input, state, and accessibility |
| WebGL | GPU-oriented rendering or very large visual workloads | More setup and shader complexity |
| Immediate raster strokes | A small sketch pad with no undo requirement | Easy to implement; resizing and undo need extra work |
| Retained stroke model | Undo, redo, replay, resizing, or collaborative editing | More state to store and redraw |
8. Troubleshooting
Canvas is blank but drawing code runs
Check that getContext('2d') did not return null, that your script runs after the canvas exists, and that you are not drawing white strokes on a white background. A canvas is intentionally blank until drawing commands execute.
Drawing is offset from the pointer
Use getBoundingClientRect() and scale coordinates by canvas.width / rect.width and canvas.height / rect.height. This is required when CSS resizes the element.
Lines look blurry
Set intrinsic dimensions, apply the device-pixel-ratio resize pattern, and scale the context. Do not rely on CSS dimensions alone.
Touch scrolls the page instead of drawing
Add touch-action: none to the canvas and use Pointer Events.
Artwork disappears after resize
Assigning canvas.width or canvas.height clears the bitmap. Keep strokes in application state and call a redraw function after resizing.
Some devices stop a stroke unexpectedly
Handle both pointerup and pointercancel, and call setPointerCapture() on pointerdown.
Canvas content is inaccessible
Canvas pixels are not exposed as semantic content. Add fallback text, labels, keyboard controls, and an alternative workflow for users who cannot draw on the canvas.
9. Performance, reliability, and cost
- Redraw only what changed for large canvases; a retained stroke model lets you optimize later.
- Use a reasonable backing resolution. A very large device-pixel-ratio multiplied bitmap consumes more memory.
- Throttle expensive exports or previews if you add them to a high-frequency pointer handler.
- Save vector-like stroke data when you need reliable undo, persistence, or replay; export PNG only at the boundary where an image is required.
- The native canvas API itself has no service charge. Hosting, storage, and any backend processing are separate costs determined by your architecture.
10. Or skip the browser setup
If your goal is to capture a rendered canvas or any webpage image rather than build the drawing surface yourself, ScreenshotNeo provides a single screenshot API request. Its capture can remove cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. An MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options.
cURL
curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'}, timeout=90)
open('shot.webp', 'wb').write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo includes full-page and element capture, dark mode, device presets, custom CSS and JavaScript, waits, request blocking, headers, cookies, geolocation, resizing, caching, signed links, asynchronous jobs, bulk capture, usage data, and PDF output. It offers 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
What is the default canvas size?
300 × 150 pixels when no intrinsic dimensions are supplied.
Can I draw without JavaScript?
No. HTML provides the canvas surface; JavaScript obtains the rendering context and handles drawing input.
Should I use CSS or HTML attributes for size?
Use attributes or DOM properties for the bitmap size, and CSS for the displayed size.
Does Pointer Events support styluses?
Yes. Pointer Events unify mouse, pen, and touch input.
Why does clearing with CSS fail?
CSS changes appearance. Use clearRect() or redraw the canvas state to remove pixels.


