Automatically scale SVG to fit width of child contents (e.g. text) [without JS]

Viewed 2827

Everything I found was the other way around, e.g. "how to scale text to fit SVG". But I want to achieve the opposite:

SVG in my html:

<svg class="text" xmlns='http://www.w3.org/2000/svg' stroke='black' fill='none'>
  <text>
    <tspan>Some Text</tspan>
  </text>
</svg>

CSS:

svg.text {
  font-size: 2.5rem; // changes on different screen sizes
  height: 0.8em; 
  width: auto; // This doesn't work... the width becomes something weird, always the same, no matter the contents of the text 
}
svg.text text {
  transform: translateY(0.8em); // needed this to make the text visible at all
}

I would like the <svg> to grow to match the width of the text element, like if a normal DOM element is set to display: inline-block; or position: absolute; or float: left;

Any ideas?

EDIT

Thanks to the comment of @enxaneta I came up with this:

JavaScript:

/**
 * Fit SVGs to their first text child's viewBox
 */
fitSvgTextElements() {
  const elements = document.querySelectorAll('svg.text');
  for( const el of elements ) {
    const box = el.querySelector('text').getBBox();
    el.setAttribute('viewBox', `${box.x} ${box.y} ${box.width} ${box.height}`);
  }
}

This works. But is there any way to solve this with pure CSS/SVG attributes?

2 Answers

The size change you want to achieve cannot be accomplished without scripting because of the basic design decisions that went into the SVG format. The spec describes the relevant concept like this:

All SVG content is drawn inside SVG viewports. Every SVG viewport defines a drawing region characterized by a size (width, height), and an origin...

The width, height and origin of SVG viewports is established by a negotiation process between the SVG document fragment generating the SVG viewport, and the parent of that fragment (whether real or implicit)...

What this amounts to is that the dimensions of the viewport are dependent on their outer context, but independent of their inner content.

Your attempt to size the viewport element by its content reverses that logical order, and that can only be achieved after the rendering has initially been done. At that point, only scripting can change the rendered DOM.

How to go about that scripted solution, you have demonstrated already in your question, allthough I would write it a bit differently. While a CSS rule width: auto; height: 1em should be sufficient, some older browser have problems with this. I would prefer to compute the width style of the <svg> element explicitely. (A width attribute would have a lower specificity than the CSS stylesheet rule.)

fitSvgTextElements() {
  const elements = document.querySelectorAll('svg.text');
  for( const el of elements ) {
    const box = el.querySelector('text').getBBox();
    el.setAttribute('viewBox', `${box.x} ${box.y} ${box.width} ${box.height}`);
    // the <svg> element is in HTML context,
    // so you need dimensions in relation to the page viewport
    const viewport = el.getBoundingClientRect();
    // retain height, recompute width in relation
    const width = viewport.height * box.width / box.height;
    el.style.width = `${width}px`;
  }
}

Actually, I found out about a cleaner approach. The magic attribute is dominant-baseline, as mentioned here.

SVG in HTML (with dominant-baseline="hanging" on the text element):

<svg class="text" xmlns='http://www.w3.org/2000/svg'>
  <text dominant-baseline="hanging">
    My Text
  </text>
</svg>

JS:

/**
 * Fit SVGs to their first text child's width and height
 */
fitSvgTextElements() {
  const elements = document.querySelectorAll('svg.text');
  for( const el of elements ) {
    const box = el.querySelector('text').getBBox();
    el.style.width = `${box.width}px`;
    el.style.height = `${box.height}px`;
  }
}
window.addEventListener('load', fitSvgTextElements);
window.addEventListener('resize', fitSvgTextElements);

...now I can adjust the text size in CSS, for example:

svg.text {
  display: block; // or inline-block
  font-size: inherit; // will respond to parent's font size
}
Related