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
svgelement embedded in HTML. Its content box, border box and device-pixel box work exactly like adiv's. When the page resizes it, you get entries. - SVG graphics elements without a CSS box —
g,path,rect,circle,textand so on inside ansvg. For these the observed size is the element's bounding box as returned bygetBBox(): the tight box around its geometry in its own user coordinate system, before transforms.contentBoxSizeandborderBoxSizeboth 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.
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.
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:
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:
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.
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:
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.
Verification Steps
- Log which elements deliver entries while resizing the window; only CSS-box elements should.
- Change a
textelement'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
svgwhose 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.
Related
- contentRect vs borderBoxSize: Which to Read — the sizes on each entry
- Keeping D3 Charts Responsive with ResizeObserver — Model B in a charting library
- Fixing Chart Libraries That Ignore Container Resize — wrapping third-party charts
↑ Back to ResizeObserver Mechanics & Triggers