TLDR:
<canvas>is a replaced element.position:fixed; inset:0will NOT size it to the viewport. Set explicitwidth:100vw; height:100vhin CSS AND size the pixel buffer fromwindow.innerWidth/Height. Two separate things, always set explicitly.
the setup
I was building presentation mode for my webinar analytics dashboard — a fullscreen, executive-grade screen-sharing view so leadership could pull up the dashboard live and walk through the numbers.
The feature commit and the fix commit landed on the same day.
That is rarely a good sign.
the wall
Content was clipping.
Not misaligned. Not flickering. Not the wrong color. Just… gone. Stuff that should have been visible was cut off, like the canvas was smaller than it appeared — or smaller than I'd told it to be.
Worked fine in normal mode. Fullscreen presentation? Cropped.
My first instinct was a CSS stacking problem. Check z-index. Check overflow. Poke the container.
All wrong direction.
the thing I keep forgetting about <canvas>
Here's the real issue: <canvas> is a replaced element.
So is <img>. So is <iframe>. And they all behave differently from a <div> in one specific, maddening way — sizing.
When you write this:
.my-canvas {
position: fixed;
inset: 0;
}
…you expect the canvas to stretch and fill the inset box. That's what a <div> does. But width:auto on a replaced element doesn't resolve to the CSS box — it resolves to the element's intrinsic size, meaning whatever width and height attributes are set directly on the <canvas> element itself.
So the canvas is sitting inside a fixed full-viewport container, looking like it fills the screen — but its actual display box is sized to its attribute dimensions. Content clips. Of course it does. The CSS box and the canvas display box are two different things, and I'd set exactly one of them.
the fix that worked
Two things, working together:
- Explicit CSS dimensions:
width:100vw; height:100vhdirectly on the canvas element — this pins the display box to the viewport - Size the pixel buffer from the window:
canvas.width = window.innerWidth * devicePixelRatio— this gives you crisp HiDPI rendering without the display size fighting you - Then
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)and draw in CSS-pixel coordinates like normal
The split is the insight worth internalizing: CSS controls the display box. Attributes control the pixel buffer. Both have to be set deliberately. Neither inherits from the other.
the same root cause hit me again (harder)
Months later I ran into the same replaced-element trap building a terrain backdrop canvas for my Apollo Dashboard — a position:fixed; inset:0; z-index:-1 canvas painting contour lines behind the whole UI.
That one actually crashed the tab. That's a different story for a different post.
But it landed the same root-cause lesson, a lot more loudly.
why this matters
Every developer has the mental model that inset:0 makes things fill the viewport. It does — for block-level elements. For replaced elements, that assumption breaks silently, and the symptoms look like CSS bugs, not canvas bugs.
Now I treat it like a checklist I don't skip: explicit width/height in CSS and buffer sizing from window.innerWidth/Height. Two separate concerns. Always set both explicitly.
The replaced-element assumption will clip your content and give you nothing to grep.