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.

Three Library Behaviours, Three IntegrationsThree categories of charting library. A window-only library needs an observer on the container calling its imperative resize method. A library that already observes internally needs nothing added, and adding a second observer risks doubling the work. A library that takes explicit dimensions needs the numbers computed and passed to its update call.listens to window onlyexposes chart.resize() or chart.update()observe the container, call its methodnew ResizeObserver(() => chart.resize())already observes internallycreates its own ResizeObserveradd nothinga second observer doubles the redrawstakes explicit dimensionsnever measures anything itselfcompute and pass the numberschart.update({ width, height })

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

TypeScript
const chart = new SomeChart(document.querySelector('#chart')!, config);
// Collapsing the sidebar does nothing. Resizing the window fixes it, proving resize() works.

Production-Safe Solution

TypeScript
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:

TypeScript
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:

TypeScript
requestAnimationFrame(() => {
  chart.resize();
  const canvas = container.querySelector('canvas');   // AFTER resize — the old node may be gone
  if (canvas) applyOverlay(canvas);
});

Deferring the Library's Write Out of the Delivery LoopTwo sequences. Calling the library resize synchronously inside the callback lets its DOM write dirty layout during the observation loop, which re-enters and eventually reports the loop limit error. Deferring the same call into an animation frame means the write lands in the next frame, so the loop settles and no warning is produced.synchronousobservation deliveredchart.resize()writes, layout dirtyloop deepens — ResizeObserver loop limit exceededdeferredobservation deliveredrequestAnimationFramechart.resize() runs in the NEXT frame — loop settles

Detecting Which Category a Library Is InThree checks to identify how a charting library handles resizing before wiring anything. Searching its source for ResizeObserver shows whether it already observes. Checking whether a documented resize or update method exists shows whether it can be driven imperatively. Resizing the window and watching whether the chart follows confirms it listens to the window at all.Three checks, before writing any integration codegrep the bundle for ResizeObserverA hit means it already observes — add nothing, or you double the redraws.look for a documented resize() or update()Its presence tells you the library can be driven imperatively.resize the window and watchIf the chart follows, it listens to window — and therefore ignores its container.

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 responsive option 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.


↑ Back to Responsive Canvas & Chart Resizing