Why box-sizing is not working with width/height attribute on canvas element?

Let's consider this code:

.canvas {
  width:150px;
  height:150px;
}
canvas {
  box-sizing:border-box;
  border:5px solid;
}
<canvas height="150" width="150"></canvas>
<canvas class="canvas"></canvas>

We have two canvas defined with the same width/height, same border and box-sizing:border-box. We can clearly see that the canvas where we used CSS properties is respecting the box-sizing (borders are reduced from the width/height) but the first one defined with attribute is ignoring box-sizing (borders are added to width/height).

I first thought it has to do with the use of attribute but it seems not as it works as intended when using img or iframe.

.canvas {
  width:150px;
  height:150px;
}
iframe,img {
  box-sizing:border-box;
  border:5px solid;
}
<iframe height="150" width="150"></iframe>
<iframe class="canvas"></iframe>

<img height="150" width="150" src="https://picsum.photos/200/300?image=1069">
<img class="canvas" src="https://picsum.photos/200/300?image=1069">

Why such behavior with canvas element?


After some search, I found that using width/height properties with canvas should be avoided as it will scale the canvas and will not resize it like we may think.

var canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d");
ctx.fillRect(0, 0, 150, 150);
canvas {
  width: 150px;
  height: 150px;
  border:1px solid red;
}
<canvas ></canvas>

Intuitively, the above code should produce a 150x150 square but we ended with a rectangle! This is because the canvas was initially 300x150 (default dimension) and our square was drawn inside. Then we scaled the whole think like we do with an image and we obtain the unwanted result.

However, using attribute will create the needed result:

var canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d");
ctx.fillRect(0, 0, 150, 150);
canvas {
  border:1px solid red;
}
<canvas height="150" width="150"></canvas>

This somehow explain why the browser is ignoring box-sizing in order to avoid the shrink effect but it still remain counter-intuitive as it may lead to other unwanted issues outside the canvas. If this was the reason, then the broswer should also do the same with img because they face the same shrink effect.

So again, why such behavior? Why the browser decide to ignore the box-sizing instead of shrinking the canvas?

Most important question: where such behavior is defined?

I am not able to find which part of the specification is dealing with this.


As a guess, I'd say it was this paragraph from the spec.

When its canvas context mode is none, a canvas element has no rendering context, and its bitmap must be fully transparent black with an intrinsic width equal to the numeric value of the element’s width attribute and an intrinsic height equal to the numeric value of the element’s height attribute, those values being interpreted in CSS pixels, and being updated as the attributes are set, changed, or removed.

That is the bitmap must be taken from the height and width attributes. Box-sizing plays no part.

Note also that the HTML5 rendering section says that:

The width and height attributes on embed, iframe, img, object or video elements, and input elements with a type attribute in the Image Button state and that either represents an image or that the user expects will eventually represent an image, map to the dimension properties width and height on the element respectively.

Canvas is not mentioned there.


A <canvas> intrinsic width and height are set by its width and height attributes.
These default to 300 and 150 respectively.

For an <img>, these will depend on the loaded resource, but for raster images, it quite simply will use the ones defined in the media.

So to make the <img> case similar to your <canvas> case, you would actually have to load a 150x150px image in the first <img> and a 300x150px in the second.

.canvas {
  width:150px;
  height:150px;
}
iframe,img {
  box-sizing:border-box;
  border:5px solid;
}
<!-- equivalent to 
<canvas width="150" height="150"></canvas>
load a 150x150px image -->
<img src="https://picsum.photos/150/150?image=1069">

<!-- equivalent to 
<canvas class="canvas"></canvas>
load a 300x150px image -->
<img class="canvas" src="https://picsum.photos/300/150?image=1069">

And now we see that <canvas> and <img> actually act exactly the same: they follow the inline-replaced elements rules (CSS2) which is getting a rewording in CSS-images-3, for about the same results.

When their computed width and height are auto, box-sizing plays no role and their intrinsic width and height are being used.
When you set it through CSS, CSS wins and box-sizing masters.