Making Chart.js fill its container: responsive, maintainAspectRatio, and the height trap
Chart.js preserves a 2:1 canvas ratio by default, so charts overflow wide cards or float in whitespace. The two-option fix, the container-height rule, and when to keep the default.
11 Nov 2025, 16:13 UTC

You drop a line chart into a dashboard card, set the card to 240 pixels tall, and the chart ignores all of it. On a wide monitor the canvas spills past the card's bottom edge; on a narrow one it floats in a band of whitespace. Nothing is broken — Chart.js is preserving a 2:1 width-to-height ratio by default, and your card is simply the wrong shape.
The fix is two options — responsive: true (already the default) and maintainAspectRatio: false — plus one rule: the chart's parent must have a height of its own. Get that pairing right and Chart.js fits any layout; get it wrong and you trade overflow for a chart that collapses to nothing.
The default is doing exactly what it says
Two options control canvas sizing, and they work together:
responsive: true(the default) — Chart.js watches the chart's parent element for size changes (using the browser'sResizeObserversince v3) and resizes the canvas to match the container's width.maintainAspectRatio: true(the default) — after matching the width, Chart.js sets the canvas height towidth / aspectRatio. For line, bar, and scatter charts the defaultaspectRatiois 2; for doughnut, pie, polarArea, and radar it is 1.
The consequence: with the defaults, canvas height is a function of width only. A 1,000-pixel-wide container produces a 500-pixel-tall canvas whether the surrounding card is 240 or 600 pixels tall. That is sensible for a chart pasted onto a plain page — it can never be stretched out of shape — but it is the wrong contract inside a designed layout, where the container's height is what you actually want respected.
The fix: two options and a container with a real height
Setting maintainAspectRatio: false changes the contract: the canvas fills the parent's content box in both dimensions, so the parent's height becomes the chart's height. Here is the pattern for a latency chart that should fill a card of fixed height and any width (examples assume Chart.js v4.x; the same options behave the same way in v3):
<div class="card">
<canvas id="latency"></canvas>
</div>/* The chart's height comes from here — it must be definite */
.card {
position: relative;
width: 100%;
height: 240px;
}new Chart(document.getElementById('latency'), {
type: 'line',
data: {
labels: ['09:00', '10:00', '11:00', '12:00', '13:00'],
datasets: [{
label: 'p95 latency (ms)',
data: [182, 204, 390, 245, 210],
tension: 0.3
}]
},
options: {
responsive: true, // the default; listed for clarity
maintainAspectRatio: false // height now comes from .card
}
});Two rules make this reliable. First, a relatively positioned wrapper is the documented pattern for responsive charts — keep it. Second, do not set CSS width or height on the canvas itself; Chart.js writes inline styles on the canvas and will fight you. Style the wrapper instead.
How to verify it
- Load the page and open your browser's developer tools.
- Select the
<canvas>element, then drag the window narrower and wider. - Run
document.querySelector('#latency').getBoundingClientRect()in the console at a few widths. - Expected result: the width tracks the card at every size, and the height stays at roughly 240 pixels. With the defaults, the height would instead have been width / 2 — 500 pixels on a 1,000-pixel-wide card.
The height rule is where this fix bites back. If the wrapper's height is auto — driven by its content — the canvas has nothing to fill: the chart can collapse to near-zero height, or in some flex and grid setups ratchet larger on every resize. The wrapper's height must resolve to a definite value: pixels, vh, a grid track, or a flex basis the layout can actually compute.
Where the trade-offs live
Round charts do not stretch — they strand. A doughnut stays a circle regardless; Chart.js fits it to the smaller canvas dimension. With maintainAspectRatio: false in a wide, short container you get a modest circle centered in a lot of dead canvas. For doughnut, pie, radar, and polarArea charts, keeping the default maintainAspectRatio: true (their aspectRatio is already 1) or giving them a square wrapper usually looks better.
Flexbox will pin the chart wide. A canvas has an intrinsic width, and flex items refuse to shrink below their content's size by default. If a chart in a flex row will not get narrower, add min-width: 0 to the flex child that contains it.
Every resize is a re-render. Chart.js re-runs layout, scale calculation, and drawing on each size change. For a heavy chart dragged through continuous window resizes, options.resizeDelay (milliseconds) defers the resize update — resizeDelay: 150 is a reasonable starting point.
Custom drawing has to keep up. Chart.js re-runs its own layout on resize, and standard plugins recompute with it. But geometry drawn outside the plugin lifecycle, or pixel coordinates cached across frames, goes stale the moment the container moves — recompute on resize or draw through a plugin hook.
A decision rule you can apply today
| Situation | Configuration |
|---|---|
| Line, bar, or scatter chart in a card or grid cell with a set height | maintainAspectRatio: false, wrapper height in CSS |
| Doughnut, pie, radar, or polarArea | Keep the default ratio, or use a square wrapper |
| Chart on a plain page with no container height | Defaults are fine; tune options.aspectRatio if 2:1 does not suit |
| Chart in a flex row that will not shrink | min-width: 0 on the chart's flex parent |
Pick the one chart in your project that misbehaves on resize, wrap it in a container with a definite height, flip maintainAspectRatio to false, and confirm with the devtools check above. If a round chart ends up swimming in empty space, revert just that chart to the default — these options are per-chart, so there is no reason to pick one sizing policy for the whole app.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.