Search code examples
svgdom

How can I center a svg text element vertically?


In the code below, I was able to determine the yOffset value by trial and error.

I would prefer to calculate the value instead so the text will be vertically centered.

How can I do that?

The documentation for the SVG Text element says:

The y coordinate of the starting point of the text baseline, or the y coordinate of each individual glyph if a list of values is provided. Value type: List of (|) ; Default value: 0; Animatable: yes

So, I need to be able to determine what the baseline is to solve this problem generally. How can I do that?

function measureSVGText(text, fontSize, fontFamily) {
  // Create an offscreen SVG element
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  
  svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
  svg.setAttribute("width", "0");
  svg.setAttribute("height", "0");
  svg.style.position = "absolute";
  svg.style.top = "-9999px";
  svg.style.left = "-9999px";

  // Create a text element
  const textElement = document.createElementNS(
    "http://www.w3.org/2000/svg",
    "text"
  );
  
  textElement.setAttribute("x", "0");
  textElement.setAttribute("y", "0");
  textElement.style.fontSize = fontSize;
  textElement.style.fontFamily = fontFamily;
  textElement.textContent = text;

  // Append the text element to the SVG
  svg.appendChild(textElement);
  document.body.appendChild(svg);

  // Get the bounding box of the text element
  const bbox = textElement.getBBox();
  const roundedBBox = svg.createSVGRect();

  roundedBBox.width = Math.ceil(bbox.width);
  roundedBBox.height = Math.ceil(bbox.height);

  // Remove the SVG element from the DOM
  document.body.removeChild(svg);

  return roundedBBox;
}

function createSVGLabel(text) {
  const padding = 4;
  const fontSize = 10;
  const fontFamily = "Roboto";
  const bbox = measureSVGText(text, `${fontSize}px`, fontFamily);

  const svgWidth = bbox.width + padding * 2;
  const svgHeight = bbox.height + padding * 2;
  const yOffset = 13;
  
  const svg =
    '<?xml version="1.0"?>' +
    `<svg width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}" version="1.1" xmlns="http://www.w3.org/2000/svg">` +
    `<rect width="${svgWidth}" height="${svgHeight}" rx="10" ry="10" style="fill:white" />` +
    `<text x="4" y="${yOffset}" font-family="${fontFamily}" font-size="${fontSize}" fill="black">${text}</text>` +
    "</svg>";

  return svg;
}

const label = createSVGLabel("00000");

const parser = new DOMParser();
const labelDocument = parser.parseFromString(label, "image/svg+xml");
const labelNode = labelDocument.getElementsByTagName("svg")[0];
const container = document.getElementById("container");

container.appendChild(labelNode);
<div id="container" style="width: 200px; height: 200px; background-color: black;">
</div>


Solution

  • Just set dominant-baseline="central"

    function measureSVGText(text, fontSize, fontFamily) {
      // Create an offscreen SVG element
      const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
      
      svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
      svg.setAttribute("width", "0");
      svg.setAttribute("height", "0");
      svg.style.position = "absolute";
      svg.style.top = "-9999px";
      svg.style.left = "-9999px";
    
      // Create a text element
      const textElement = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "text"
      );
      
      textElement.setAttribute("x", "0");
      textElement.setAttribute("y", "0");
      textElement.style.fontSize = fontSize;
      textElement.style.fontFamily = fontFamily;
      textElement.textContent = text;
    
      // Append the text element to the SVG
      svg.appendChild(textElement);
      document.body.appendChild(svg);
    
      // Get the bounding box of the text element
      const bbox = textElement.getBBox();
      const roundedBBox = svg.createSVGRect();
    
      roundedBBox.width = Math.ceil(bbox.width);
      roundedBBox.height = Math.ceil(bbox.height);
    
      // Remove the SVG element from the DOM
      document.body.removeChild(svg);
    
      return roundedBBox;
    }
    
    function createSVGLabel(text) {
      const padding = 4;
      const fontSize = 10;
      const fontFamily = "Roboto";
      const bbox = measureSVGText(text, `${fontSize}px`, fontFamily);
    
      const svgWidth = bbox.width + padding * 2;
      const svgHeight = bbox.height + padding * 2;
      
      const svg =
        '<?xml version="1.0"?>' +
        `<svg width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}" version="1.1" xmlns="http://www.w3.org/2000/svg">` +
        `<rect width="${svgWidth}" height="${svgHeight}" rx="10" ry="10" style="fill:white" />` +
        `<text x="4" y="${svgHeight / 2}" font-family="${fontFamily}" font-size="${fontSize}" fill="black" dominant-baseline="central">${text}</text>` +
        "</svg>";
    
      return svg;
    }
    
    const label = createSVGLabel("00000");
    
    const parser = new DOMParser();
    const labelDocument = parser.parseFromString(label, "image/svg+xml");
    const labelNode = labelDocument.getElementsByTagName("svg")[0];
    const container = document.getElementById("container");
    
    container.appendChild(labelNode);
    <div id="container" style="width: 200px; height: 200px; background-color: black;">
    </div>