The canvas's width and height set the texture size; CSS only stretches it. Setting them replaces the drawing buffer, and the next getCurrentTexture() has the new size without a new configure(). Sizes above maxTextureDimension2D fail validation, so clamp them. A ResizeObserver with devicePixelContentBoxSize gives the exact device-pixel size (Chrome 1 and Firefox 555 ; Safari 10 needs the CSS size times devicePixelRatio):
<style>canvas { width: 100%; height: 60px; display: block }</style>
<canvas id="shelf"></canvas>
<script type="module">
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const context = shelf.getContext('webgpu');
context.configure({ device, format: navigator.gpu.getPreferredCanvasFormat() });
const clamp = (n) => Math.min(n, device.limits.maxTextureDimension2D);
new ResizeObserver(([entry]) => {
const css = entry.contentBoxSize[0], box = entry.devicePixelContentBoxSize?.[0];
shelf.width = clamp(box?.inlineSize ?? Math.round(css.inlineSize * devicePixelRatio));
shelf.height = clamp(box?.blockSize ?? Math.round(css.blockSize * devicePixelRatio));
const t = context.getCurrentTexture();
console.log(`CSS ${css.inlineSize}x${css.blockSize} -> texture ${t.width}x${t.height}`);
}).observe(shelf);
</script>Run with Chrome's display scale forced to 1.5 (--force-device-scale-factor=1.5):
CSS 464x60 -> texture 696x90
Redraw after a resize, since the contents are gone, and recreate depth and multisample textures to match (Depth and Multisampling). Draw from requestAnimationFrame, not the observer, so a drag still draws once per frame.