Skip to content
Advertisement

getBoundingClientRect() returns inaccurate values for complex SVG’s in Chrome

I’m trying to calculate the bounding box of transformed SVG elements and for that I’m using getBoundingClientRect() and mapping the x and y values to SVG coordinates. However, this function seems to produce wrong outputs in Chrome and Edge when the shape has curves and a rotation. In the other hand, Firefox is able to produce the expected result.

Here’s an example.

<svg height="600" width="600">
  <g transform="rotate(-50, 240, 174)" fill="#A1B6FF">        
    <path transform="translate(100, 100)" 
    d="M0, 0 Q 140 128.76 280 0 v 148 Q 140 276.76 0 148 v -148z">
    </path>
  </g>
</svg>

Is there any way to achieve this with more precision like Firefox?

Answer

I deleted my previous answer as it was plain wrong, hope this is a better one:

<div>
    <svg id="svg" width="600" height="600" version="1.1" viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg">
        <g id="svgElem" transform="rotate(-50, 240, 174)" fill="#A1B6FF">
            <path transform="translate(100, 100)"
                    d="M0, 0 Q 140 128.76 280 0 v 148 Q 140 276.76 0 148 v -148z">
            </path>
        </g>
    </svg>
</div>

<script type="text/javascript">        
    let svgElem = document.getElementById('svgElem');
    
    let bBox = svgElem.getBBox();
    
    console.dir(bBox);

</script>

The SVGRect returned by getBBox is identical Firefox/Chromium. However as stated here on MDN

The returned value is a SVGRect object, which defines the bounding box. This value is irrespective of any transformation attribute applied to it or the parent elements

So you always get the bounding box of the svg before tranforms are applied this way. If you use getBoundingClientRect to get a DOMRect you will find out that Chrome seems to just apply the transforms on the original bounding rect and then calculate the bounding box of that.

enter image description here

You would achieve the same with something like this (more or less useless code just for illustration):

<script type="text/javascript">
    const svg = document.getElementById('svg');
    let svgElem = document.getElementById('svgElem');

    const bBox = svgElem.getBBox(); // MDN: The returned value is a SVGRect object, which defines the bounding box. This value is irrespective of any transformation attribute applied to it or the parent elements
    console.dir(bBox);

    const boundingClientRect = svgElem.getBoundingClientRect(); 
    console.dir(boundingClientRect);

    // create a rect without transforms
    const rect1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    rect1.setAttribute('x', bBox.x);
    rect1.setAttribute('y', bBox.y);
    rect1.setAttribute('width', bBox.width);
    rect1.setAttribute('height', bBox.height);
    rect1.setAttribute('fill', '#00ff0040');
    svg.appendChild(rect1);

    const ctm = svgElem.getCTM();

    const topLeftX = ctm.a * bBox.x + ctm.c * bBox.y + ctm.e;
    const topLeftY = ctm.b * bBox.x + ctm.d * bBox.y + ctm.f;

    const topRightX = ctm.a * (bBox.x + bBox.width) + ctm.c * bBox.y + ctm.e;
    const topRightY = ctm.b * (bBox.x + bBox.width) + ctm.d * bBox.y + ctm.f;

    const bottomLeftX = ctm.a * bBox.x + ctm.c * (bBox.y + bBox.height) + ctm.e;
    const bottomLeftY = ctm.b * bBox.x + ctm.d * (bBox.y + bBox.height) + ctm.f;

    const bottomRightX = ctm.a * (bBox.x + bBox.width) + ctm.c * (bBox.y + bBox.height) + ctm.e;
    const bottomRightY = ctm.b * (bBox.x + bBox.width) + ctm.d * (bBox.y + bBox.height) + ctm.f;

    const x = Math.min(topLeftX, topRightX, bottomLeftX, bottomRightX);
    const y = Math.min(topLeftY, topRightY, bottomLeftY, bottomRightY);
    const width = Math.max(topLeftX, topRightX, bottomLeftX, bottomRightX) - x;
    const height = Math.max(topLeftY, topRightY, bottomLeftY, bottomRightY) - y;

    const rect2 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    rect2.setAttribute('x', x);
    rect2.setAttribute('y', y);
    rect2.setAttribute('width', width);
    rect2.setAttribute('height', height);
    rect2.setAttribute('fill', '#ff000040');
    svg.appendChild(rect2);

</script>

Or you could just check the Developer tools of Firefox/Chromium to see the dfifferences (just to say putting a group around doesn’t work either).

Maybe SVG version 2 will make a difference in the future: Chrome Platfor Status SVG2

So now what? If getBBox is the only function that seems to be working but only for svgs without inner transforms, can these transforms be applied dynamically with javascript?

Turns out someone went the extra mile: flatten.js

put the script in a file ‘flatten.js’ and remove the leftovers at the top if still there (html, title..)

<div>
    <svg id="svg" width="600" height="600" version="1.1" viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg">
        <g id="svgElem" transform="rotate(-50, 240, 174)" fill="#A1B6FF">
            <path transform="translate(100, 100)"
                  d="M0, 0 Q 140 128.76 280 0 v 148 Q 140 276.76 0 148 v -148z">
            </path>
        </g>
    </svg>
</div>

<script src="flatten.js"></script>

<script type="text/javascript">
    const svg = document.getElementById('svg');
    let svgElemClone = document.getElementById('svgElem').cloneNode(true); // flatten will directly change the element so a clone is made
    svgElemClone.id = 'svgElemClone';
    svg.appendChild(svgElemClone);

    flatten(svgElemClone, true);

    const bBox = svgElemClone.getBBox();
    console.dir(bBox);        
</script>    

enter image description here

enter image description here

So this might be a workaround way to get the “real” bounding box.

As for getBoundingClientRect: MDN says: “The returned value is a DOMRect object which is the smallest rectangle which contains the entire element, including its padding and border-width.”

IMHO there is a bug in Chromium’s implementation.

Advertisement