A chart that resizes correctly when you drag the browser window and ignores a collapsing sidebar is not broken. It is listening to the wrong thing, and the fix is to give it a signal it was never designed to receive.
Problem / Scenario Context
A dashboard puts a chart in the main region beside a collapsible sidebar. Collapsing the sidebar widens the main region by 240 pixels. The chart stays exactly the size it was, with its rendered surface stretched or letterboxed inside the new box, until the window itself is resized — at which point it snaps to the correct size, confirming that the library can resize and simply was not told to.
The cause is that window.resize fires only when the viewport changes. Every layout change that does not touch the viewport — a sidebar, a split pane, an accordion, a font finishing loading, a flex sibling growing — is invisible to it. This is precisely the gap element resize detection exists to close.
Mechanics Explanation
Libraries fall into three groups, and the correct integration differs for each.
Window-only. The library attaches its own window resize listener and exposes an imperative resize() or update(). You observe the container and call that method. This is the common case and the easiest.
Already observing. Newer libraries create their own ResizeObserver internally. Adding a second one means two observers driving the same redraw, which at best doubles the work and at worst produces a feedback loop if the library writes back into the observed element. Check first, and if it is already handled, do nothing.
Explicit dimensions only. The library takes width and height as configuration and never measures. You compute the numbers and pass them in, which is the most work but also the most predictable.
The one behaviour to watch for across all three is whether the library's resize path recreates its canvas element. Several do, which invalidates any reference you cached.
Comparison Table: Signals Available to a Library
| Layout change | window.resize |
ResizeObserver on the container |
|---|---|---|
| Browser window resized | fires | fires |
| Sidebar collapsed | silent | fires |
| Split pane dragged | silent | fires |
| Accordion opened above the chart | silent | fires |
| Web font loaded, text rewraps | silent | fires |
| Device rotated | fires | fires |
| Chart moved to a different grid area | silent | fires |
Five of seven rows are the gap. That is why a library relying only on window looks correct during development — where the window is the thing you resize — and wrong in a real dashboard.
Minimal Reproducible Example
const chart = new SomeChart(document.querySelector('#chart')!, config);
// Collapsing the sidebar does nothing. Resizing the window fixes it, proving resize() works.
Production-Safe Solution
type Resizable = { resize?: () => void; update?: (o?: unknown) => void; destroy?: () => void };
function bindContainerResize(container: HTMLElement, chart: Resizable): () => void {
let queued = false;
let lastW = 0, lastH = 0;
const ro = new ResizeObserver((entries) => {
const box = entries[entries.length - 1].contentBoxSize?.[0];
const w = Math.round(box ? box.inlineSize : entries[0].contentRect.width);
const h = Math.round(box ? box.blockSize : entries[0].contentRect.height);
if (w === 0 || h === 0) return; // hidden tab or collapsed panel
if (w === lastW && h === lastH) return;
lastW = w; lastH = h;
if (queued) return;
queued = true;
// deferring to the next frame keeps the library's own writes out of the delivery loop
requestAnimationFrame(() => {
queued = false;
if (typeof chart.resize === 'function') chart.resize();
else if (typeof chart.update === 'function') chart.update({ width: lastW, height: lastH });
});
});
ro.observe(container);
return () => ro.disconnect();
}
Three details are load-bearing.
The zero guard. A chart in a hidden tab reports 0 × 0, and many libraries either throw or reconfigure themselves to a degenerate size when handed zero. Returning early leaves the chart untouched until the panel is shown again, at which point a real measurement arrives.
The animation frame. The library's resize() will write to the DOM. Doing that synchronously inside the delivery loop is what produces ResizeObserver loop limit exceeded, especially for libraries that adjust their container's height. Deferring by one frame moves the write out of the loop.
Disabling the library's own listener when it has one, so the chart is not resized twice per window resize:
const chart = new SomeChart(el, { ...config, responsive: false }); // library-specific
const stop = bindContainerResize(el, chart);
For a library that recreates its canvas, re-query rather than caching:
requestAnimationFrame(() => {
chart.resize();
const canvas = container.querySelector('canvas'); // AFTER resize — the old node may be gone
if (canvas) applyOverlay(canvas);
});
Verification Steps
- Collapse the sidebar and confirm the chart resizes without touching the window.
- Watch the console during a pane drag; the loop-limit warning means the call is still synchronous.
- Count redraws with a counter inside
resize(); two per window resize means the library's own listener is still active. - Hide the panel and reopen it to confirm the zero guard leaves the chart intact rather than collapsing it.
- Check for a recreated canvas by holding a reference across a resize and testing
container.contains(cached).
Common Mistakes to Avoid
- Observing the chart's canvas rather than the container. The library resizes that canvas, so you are observing the thing you are about to change.
- Leaving the library's
responsiveoption on. Two resize paths fight, and the visible symptom is a chart that flickers between two sizes. - Calling
resize()synchronously in the callback. The most common source of the loop-limit warning in dashboard code. - Caching the canvas across a resize. Several libraries replace it, and the cached node is detached.
FAQ
Why does my chart only resize when I resize the browser window?
Because the library is listening for the window resize event, which fires only when the viewport changes. A sidebar collapsing, a pane being dragged or a font loading all change the chart's container without touching the viewport, so the event never fires and the library is never told.
Should I add a ResizeObserver if the library already has one?
No. Two observers driving the same redraw at best doubles the work and at worst creates a feedback loop if the library writes back into the element you are observing. Check the library's documentation or its source for an internal observer before adding your own.
Why does calling chart.resize() cause the loop limit warning?
Because the library writes to the DOM, and doing that synchronously inside the observation callback dirties layout while the browser is still delivering observations. Deferring the call into a requestAnimationFrame moves the write into the next frame, which is enough for the loop to settle.
What should happen when the chart's panel is hidden?
Nothing. A hidden container reports zero by zero, and passing that to a chart library either throws or reconfigures it to a degenerate size that survives the panel being shown again. Guard for zero and return early; a real measurement arrives when the panel reopens.
Related
- Responsive Canvas & Chart Resizing — the sizing rules the library is applying on your behalf
- Debouncing Chart Redraws on Container Resize — how often to call the method once it is wired
- Element Resize Detection Patterns — why element-level observation replaced window resize
↑ Back to Responsive Canvas & Chart Resizing