Fixing Blurry HTML5 Canvas on High‑DPI Screens
Learn why HTML5 Canvas looks blurry on Retina screens and how to fix it by aligning the backing store with devicePixelRatio.
06 Aug 2025, 00:40 UTC

The problem: canvas looks fuzzy on Retina displays
You add a <canvas> element, draw a sharp line or text, and on a laptop with a Retina screen or a 4K monitor the result appears blurry. The CSS‑sized canvas is correct, but the bitmap inside is stretched to fit more physical pixels.
Why it happens
The <canvas> element has two size concepts:
- the CSS size that controls how much space it takes up in the layout, and
- the backing store (the actual pixel buffer) that the 2D context draws into.
- If you set only CSS width/height (e.g.,
width: 400px; height: 300px;), the backing store defaults to 400×300 device‑independent pixels. - On a screen with
devicePixelRatio = 2, each CSS pixel maps to 2×2 physical pixels, so the browser upscales the 400×300 buffer, causing blur.
Solution: match backing store to devicePixelRatio
Before drawing, set the canvas’s width and height attributes (not CSS) to the CSS size multiplied by window.devicePixelRatio. Then scale the drawing context back so that 1 unit in user space equals 1 CSS pixel.
Worked example
// Get the canvas element (run in the browser console or a script tag)
const canvas = document.querySelector('#myCanvas');
const ctx = canvas.getContext('2d');
// Desired CSS size
const cssWidth = 400;
const cssHeight = 300;
// Set CSS size (could also be done via a stylesheet)
canvas.style.width = `${cssWidth}px`;
canvas.style.height = `${cssHeight}px`;
// Adjust backing store for high‑DPI
const dpr = window.devicePixelRatio || 1;
canvas.width = cssWidth * dpr;
canvas.height = cssHeight * dpr;
// Scale the context so drawing uses CSS pixels
ctx.scale(dpr, dpr);
// Now draw as usual – lines will be crisp
ctx.lineWidth = 2;
ctx.strokeStyle = '#0066cc';
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(350, 250);
ctx.stroke();
Place the script after the canvas markup or run it on DOMContentLoaded. The same logic works in a framework’s lifecycle hook (e.g., React useEffect or Vue mounted).
Trade‑offs and limitations
- Increased memory: the backing store now holds
dpr²times more pixels. On a 4K screen (dpr≈2) a 400×300 canvas uses 4× the memory. - If you animate many canvases, consider offscreen canvases or requestAnimationFrame to keep the main thread responsive.
- Scaling the context (
ctx.scale) affects all subsequent drawing operations; remember to reset (ctx.setTransform(1,0,0,1,0,0)) if you need to draw in device pixels for a specific effect.
How to verify the fix
- Open the page on a device with known high‑DPI (e.g., a MacBook Pro Retina) and open DevTools.
- Select the
<canvas>element; in the “Computed” tab note the CSS width/height. - In the console, read
canvas.widthandcanvas.height. They should equal CSS size ×window.devicePixelRatio. - Visually inspect a thin line or small text; it should appear as sharp as surrounding HTML text.
If the line still looks fuzzy, check that you are not overriding the canvas size with CSS after the script runs, and that any subsequent ctx.setTransform calls do not cancel the scale.
Actionable closing
For any canvas‑based visualization, game, or image‑processing widget, add the backing‑store adjustment routine at initialization. It costs only a few lines of code, eliminates the common blurry‑canvas issue on high‑DPI displays, and works across all modern browsers that support the HTML5 Canvas 2D context.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.