Observe the outer svg element (or its HTML container) for responsive charts — it has a CSS box like any other element — and treat inner shapes as a special case: they report their bounding box from getBBox() in user units, ignoring transform and viewBox scaling, so they rarely change in the way you expect.

Problem / Scenario Context

A data visualisation team builds charts as inline SVG. They observe the <g class="plot"> group inside the chart to re-layout axis labels when the plot area changes size. The callback fires once on load and then never again, no matter how the window is resized, because the chart scales through its viewBox and the group's own coordinates never change. Meanwhile, a separate icon component observes a <path> that animates with a transform: scale() and sees no entries at all.

SVG elements live in a different coordinate world from HTML boxes. The ResizeObserver Mechanics & Triggers topic covers HTML boxes; this page covers what changes with SVG.

Mechanics Explanation

The Resize Observer specification defines two kinds of observable elements:

  • Elements with a CSS layout box — including the outer svg element embedded in HTML. Its content box, border box and device-pixel box work exactly like a div's. When the page resizes it, you get entries.
  • SVG graphics elements without a CSS boxg, path, rect, circle, text and so on inside an svg. For these the observed size is the element's bounding box as returned by getBBox(): the tight box around its geometry in its own user coordinate system, before transforms. contentBoxSize and borderBoxSize both report that bounding box.

Two consequences follow. When the outer SVG scales through its viewBox, the inner coordinates do not change, so inner elements report no resize. And a transform (attribute or CSS) on an inner element does not change its getBBox(), so transform animations are invisible to the observer. Inner elements do report changes to their own geometry: a rect whose width attribute changes, a path whose d changes, a text whose content grows.

What Each SVG Target ReportsA grid of SVG targets and whether they deliver entries for three kinds of change. The outer svg element delivers when its CSS size changes, not for inner geometry changes. An inner group or shape does not deliver for CSS size or viewBox scaling, does not deliver for transforms, but does deliver when its own geometry attributes change.Outer CSS size changesTransform or viewBox scaleOwn geometry changesouter svg elementdeliversn/anog groupnonodelivers (children's bbox)rect / path / circlenonodeliverstextnonodelivers when content changes

Comparison Table: Choosing a Target for SVG Work

Goal Observe Read Notes
Responsive chart layout outer svg or its HTML wrapper contentBoxSize[0] CSS pixels on the page
Crisp stroke widths when scaled outer svg device-pixel box or use vector-effect: non-scaling-stroke
Label that grows with its text the text element bounding box user units, pre-transform
Fit a background rect behind a label the text element bounding box then set rect attributes
Icon animating with a transform nothing — transforms are not resizes animate, do not observe

Minimal Reproducible Example

The chart markup is a full-width wrapper div.chart containing an SVG with viewBox="0 0 600 300" and inline-size: 100%, whose plot area is a g.plot group holding a 600×300 rect.

TypeScript
const ro = new ResizeObserver((es) => es.forEach((e) =>
  console.log(e.target.nodeName, Math.round(e.contentBoxSize[0].inlineSize))));
ro.observe(document.querySelector('.plot')!);          // logs 600 once, never again
ro.observe(document.querySelector('.chart svg')!);     // logs the CSS width on every resize

Resize the window: only the svg line repeats. The group's bounding box is 600 user units wide regardless of how large the chart is drawn.

Production-Safe Solution

For charts, choose between two models and observe accordingly.

Model A — scale with the viewBox. The drawing is authored in fixed user units and scaled uniformly. Observe the outer svg only to adjust things that must not scale — stroke widths, font sizes, tick density:

TypeScript
const svg = document.querySelector<SVGSVGElement>('.chart svg')!;
const VIEW_W = 600;

new ResizeObserver(([e]) => {
  const cssW = e.contentBoxSize[0].inlineSize;
  const scale = cssW / VIEW_W;                            // user units → CSS px
  // Keep labels ~12 CSS px and strokes ~1.5 CSS px whatever the scale.
  svg.style.setProperty('--label-size', `${12 / scale}px`);
  svg.style.setProperty('--stroke', `${1.5 / scale}px`);
  svg.dataset.ticks = cssW < 400 ? 'sparse' : 'dense';
}).observe(svg);

Model B — redraw in CSS pixels. Remove the fixed viewBox scaling and redraw the chart at the container's actual size, so one user unit equals one CSS pixel. Observe the HTML wrapper and recompute the scales:

TypeScript
const wrapper = document.querySelector<HTMLElement>('.chart')!;
const svgB = wrapper.querySelector<SVGSVGElement>('svg')!;

new ResizeObserver(([e]) => {
  const w = Math.round(e.contentBoxSize[0].inlineSize);
  const h = Math.round(w * 0.5);
  svgB.setAttribute('viewBox', `0 0 ${w} ${h}`);
  svgB.setAttribute('height', String(h));
  renderChart(svgB, w, h);                                // axes and marks in CSS px
}).observe(wrapper);

declare function renderChart(svg: SVGSVGElement, w: number, h: number): void;

Model B observes the wrapper rather than the svg, because the callback writes the svg's height; observing the element you resize invites the loop described in fixing ResizeObserver loop limit exceeded.

Scale the Drawing or Redraw ItTwo columns. Scaling with the viewBox keeps the drawing in fixed user units and scales everything uniformly; the observer only compensates labels and strokes that must not scale. Redrawing in CSS pixels sets the viewBox to the container size on every resize and recomputes axes and marks, giving exact pixel control at the cost of a redraw.Model A: scale with viewBoxDrawing authored once in fixed user unitsObserver adjusts labels and strokes onlyCheap; aspect ratio fixedModel B: redraw in CSS pixelsviewBox matches container size each resizeAxes, ticks and marks recomputedExact control; costs a redraw

Observing Inner Elements on Purpose

Inner-element observation is useful for one job: reacting to content size inside a drawing. A tooltip label whose text changes needs a background rectangle that fits it; observing the text element delivers its new bounding box whenever the content changes, in the same frame, and the callback can size the rectangle:

TypeScript
const label = document.querySelector<SVGTextElement>('.tooltip text')!;
const bg = document.querySelector<SVGRectElement>('.tooltip rect')!;

new ResizeObserver(([e]) => {
  const { inlineSize: w, blockSize: h } = e.contentBoxSize[0];
  const bb = label.getBBox();                           // for x/y; size comes from the entry
  bg.setAttribute('x', String(bb.x - 6));
  bg.setAttribute('y', String(bb.y - 4));
  bg.setAttribute('width', String(w + 12));
  bg.setAttribute('height', String(h + 8));
}).observe(label);

The rectangle is a sibling, not an ancestor, of the text, so resizing it does not change the observed bounding box and cannot loop.

A Label Background That Follows Its TextThree boxes. The tooltip text changes content. The ResizeObserver on the text element delivers its new bounding box in the same frame. The callback resizes the sibling background rectangle, which does not affect the text's bounding box, so no further entries follow.Text content changesnew label stringRO on textbounding box delivered, same frameResize sibling rectno effect on text, no loop

Verification Steps

  • Log which elements deliver entries while resizing the window; only CSS-box elements should.
  • Change a text element's content and confirm its observer fires.
  • Apply a transform to an observed inner element and confirm no entry — expected behaviour.
  • Zoom the page and confirm stroke and label compensation keeps them visually constant in Model A.
  • Check for loop errors when redrawing in Model B; there should be none with the wrapper observed.

Common Mistakes to Avoid

  • Observing an inner group to detect chart resizes. Its user-unit bounding box does not change when the chart scales.
  • Expecting transforms to produce entries. They do not change getBBox().
  • Observing the svg whose height the callback sets. Observe a wrapper instead.
  • Mixing coordinate systems. Entry sizes for inner elements are user units; for the outer svg, CSS pixels.

FAQ

Can ResizeObserver observe SVG elements at all?

Yes. The outer svg element behaves like any CSS box. Inner SVG graphics elements are observable too, and report their bounding box in user units.

Why does my observed path never fire when the chart resizes?

Because the path's geometry, measured in the SVG's user coordinate system, does not change when the whole drawing is scaled by the viewBox. Observe the outer svg or its container instead.

Do CSS transforms on SVG elements trigger ResizeObserver?

No. Transforms change how an element is painted, not its bounding box, so no entry is delivered.

What is the difference between contentBoxSize and borderBoxSize for SVG shapes?

For inner SVG elements without a CSS box, both report the same bounding box. Stroke width is not included, since getBBox measures geometry only.

Does an svg element used as an img source get observed the same way?

An SVG loaded through an img element or a CSS background is an image, not a DOM subtree you can reach. Observe the img element itself, which has an ordinary CSS box; the drawing inside scales with it and cannot be observed separately.

Should I use vector-effect instead of observing?

For stroke widths, yes: vector-effect: non-scaling-stroke keeps strokes a constant screen width with no script at all. Observing is still needed for font sizes, tick density and other decisions CSS cannot make.


↑ Back to ResizeObserver Mechanics & Triggers