Skip to content

JavaScript Canvas Fix Draw Rectangles on Images Correctly

A developer in a JavaScript community I participate in recently ran into a classic hurdle with the HTML5 Canvas API. They were able to get their image to appear on the screen, but their code to draw a rectangle over that image seemingly did nothing. The code looked logically sound, yet the rectangle remained invisible.

This is a common “gotcha” when working with the Canvas API, usually caused by either the asynchronous nature of image loading or the order of operations in the drawing state.

“I am trying to draw a rectangle on top of an image using the Canvas element in JavaScript. The image displays perfectly, but the rectangle does not display at all. What am I doing wrong?”

In 90% of these cases, the issue is that the rectangle is being drawn before the image has finished loading, or the image is being drawn over the rectangle.

Here is the most robust way to ensure your shapes appear on top of your images.

// Illustrative example — JavaScript ES6 / HTML5 Canvas
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'path/to/your-image.jpg';
// FIX 1: Use the onload callback to ensure the image is ready
img.onload = () => {
// 1. Draw the image first
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// 2. Set styles for the rectangle
ctx.strokeStyle = 'red';
ctx.lineWidth = 5;
// 3. Draw the rectangle AFTER the image
ctx.strokeRect(50, 50, 150, 100);
};

When you set img.src, the browser starts downloading the image in the background. If your next line of code is ctx.strokeRect(), the JavaScript engine executes it immediately. Since the image hasn’t finished loading, the canvas is empty, the rectangle is drawn, and then—milliseconds later—the image finally loads and triggers a different part of your code (or you manually call drawImage), which wipes over the rectangle you just drew.

The HTML5 Canvas uses a “Painter’s Algorithm.” This means that whatever you draw last is placed on top of whatever was drawn before it. There is no built-in “z-index” for shapes on a single canvas context. If you call ctx.strokeRect() and then call ctx.drawImage(), the image will completely opaque the rectangle.

Another common mistake is using ctx.rect() without calling a drawing command.

  • ctx.rect(x, y, w, h) only defines a path; it does not paint anything.
  • You must call ctx.stroke() or ctx.fill() to actually make the pixels appear.
  • Alternatively, use the shorthand ctx.strokeRect() or ctx.fillRect().

Solution A: The Promise-Based Approach (Modern Clean Code)

Section titled “Solution A: The Promise-Based Approach (Modern Clean Code)”

If you are working in a modern environment (Node 20+, React 18, or modern browsers), using Promises makes the drawing logic much easier to read and prevents “callback hell.”

// Illustrative example — ES6+
async function drawLayeredCanvas() {
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const loadImage = (url) => new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
try {
const myImage = await loadImage('hero-shot.png');
// Clear canvas before drawing
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw bottom layer
ctx.drawImage(myImage, 0, 0);
// Draw top layer
ctx.lineWidth = 2;
ctx.strokeStyle = '#00FF00';
ctx.strokeRect(10, 10, 100, 100);
} catch (err) {
console.error("Image failed to load", err);
}
}
drawLayeredCanvas();

Solution B: The RequestAnimationFrame Loop (For Dynamic Apps)

Section titled “Solution B: The RequestAnimationFrame Loop (For Dynamic Apps)”

If your rectangle needs to move (like a bounding box in AI detection), you should use a render loop. This ensures the canvas is cleared and redrawn constantly.

// Illustrative example — verify in your environment
let boxX = 0;
function render() {
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 1. Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 2. Draw the image (assuming img is already pre-loaded)
ctx.drawImage(loadedImg, 0, 0);
// 3. Draw the dynamic rectangle
ctx.strokeStyle = 'blue';
ctx.strokeRect(boxX, 50, 50, 50);
boxX += 1; // Move the box
requestAnimationFrame(render);
}

This usually happens because the Canvas’s internal resolution (attribute width/height) doesn’t match its CSS size.

  • Correct: <canvas width="800" height="600"></canvas>
  • Incorrect: Setting width/height via <canvas style="width: 800px; height: 600px;"> The CSS scales the image, causing pixelation. Always set the element attributes to match the aspect ratio of your image.

Can I draw rectangles on a separate layer?

Section titled “Can I draw rectangles on a separate layer?”

Yes. If you are doing complex manipulations, the best practice is to use two canvas elements stacked on top of each other using CSS position: absolute.

  1. Background Canvas: Draw the image here once.
  2. Foreground Canvas: Draw your rectangles/UI here. This is significantly more performant because you don’t have to redraw the heavy image every time a small rectangle moves.

If you load an image from a different domain (CDN) and try to draw it to a canvas, you might encounter a “Tainted Canvas” error when trying to export the data. Always add img.crossOrigin = "anonymous"; before setting the src if you plan to use canvas.toDataURL().