skip to Main Content

I am trying to position a svg with text in exactly the same position as div with text, no matter the font family used or font size

The problem comes with the first element being offset in x and y position.

I am trying to set both elements in top 0 left 0 absolute position but they fail to match .

Of course manually adding correct offset x and y works, but I want a general solution.

Here is an example that if you run, the texts won’t be positioned correctly one on top of another.
I am trying to make this work with any font, any size.

<div style="position:absolute;color:red;font-family:Verdana;font-size:20px; margin-left:0; padding-left:0; left:0; ">
  First Line
</div>
<div style="position:absolute;top:0;left :0; background:transparent;"> 
<svg width="100" height="20" xmlns="http://www.w3.org/2000/svg">
<text x="0" y="0" font-family="Verdana" text-anchor="start" dominant-baseline="hanging" font-size="20" fill="black">First Line</text>
</svg>
</div>

Here is a codepen also https://codepen.io/Cristian-M/pen/VwJzXJv

Modifying the font size (make it bigger) and the offset is visible more and more

Any ideas?
Thanks!

2

Answers


  1. @blaster god i think you will need to put both elements inside a wrapper. Set the wrapper to relative positioning. Then, set both elements to absolute positioning with top and left set to 0. Also, make sure the div’s line height is the same as its font size. Use the same font size and font family for both element and remove that extra a

    First Line

    First Line

    Login or Signup to reply.
  2. I’m afraid you can only partially solve this task. Mainly due to SVG’s text-composing and layout limitations that make a perfect (responsive) replication of HTML elements quite impossible:

    • no line-wrapping in SVG (I hope I need to revise this when e.g inline-size get’s finalized in the W3C specs and adopted – I won’t hold my breath …)
    • no automatic content based bounding box adjustments (not to speak of CSS layout features like flex or grid)

    In other words: to emulate/replicate HTML layouts in SVG we need to calculate suitable values via JavaScript.

    Approach 1: inject SVG as inlined elements

    We’re basically inheriting most of the the HTML font properties to the SVG <text> element as we’re rather injecting the SVG "lookalike" into the existing HTML/CSS layout context. Most notably we’re setting the SVG text-baseline to the bottom of the viewBox and apply overflow:visible to clipped descenders or ascenders: the SVG element will move like a HTML text element respecting the inherited font-size.

    let svg = document.querySelector('.svgText')
    let {width} = svg.getBBox();
    
    // adjust viewBox
    svg.setAttribute('viewBox', [0,0, width, 1].join(' '));
    
    
    // fontSize change
    inputFontSize.addEventListener('input', e=>{
      let fontSize = +e.currentTarget.value;
      document.body.style.fontSize = `${fontSize}px`;
    })
    body {
      font-family: verdana;
      font-size: 80px;
    }
    
    * {
      box-sizing: border-box;
    }
    
    .fnt-siz, .tools,
    h3 {
      font-size: 16px;
      line-height: 1.2em;
    }
    
    .tools {
      position: sticky;
      top: 0;
      background: #000;
      color: #fff;
      padding: 0.5em;
      font-size: 20px;
      line-height: 1.2em;
    }
    
    .htmlText {
      position: absolute;
      background: yellow;
      color: red;
      margin-left: 0;
      padding-left: 0;
      left: 0.5em;
      top: 2em;
    }
    
    svg {
      font-size: 1px;
      letter-spacing: inherit;
    }
    
    .svgTextOverlay {
      position: relative;
    }
    
    svg {
      font-size: 1em;
      height: 1em;
      overflow: visible;
      outline: 2px dotted red;
    }
    
    .svgText {
      position: absolute;
      left: 0;
      top: 0;
    }
    <div class="tools">
      <p><label>fontsize <input id="inputFontSize" type="range" value="80" min="10" max="200"></label></p>
    </div>
    
      <h3>Synchonize SVG with HTML font-size</h3>
      <div class="htmlText">
        <span class="svgTextOverlay">
          Hamburg fonts
          <!-- svg text overlay -->
          <svg id="svg1" class="svgText" viewBox="0 0 1 1"  >
            <text x="0" y="1" font-size="1" >Hamburg fonts</text>
          </svg>
        </span>
      </div>

    As you can see, we can reproduce the HTML text properties quite accurately… unless we get a line break.

    The most simplistic SVG text replacement placeholder could be something like this:

    SVG

    <svg class="svgText" viewBox="0 0 1 1">
      <text y="1" font-size="1">
        Your text
      </text>
    </svg>
    

    The required CSS to make the SVG "float" on the baseline like regular text (and also inherit properties like text color) would be:

    CSS

    .svgText{
      display:inline-block;
      font-size: 1em;
      height: 1em;
      overflow: visible;
      fill: currentColor;
    }
    

    JS

    Required to calculate a suitable viewBox width

    let {width} = svg.getBBox();
    svg.setAttribute('viewBox', [0,0, width, 1].join(' '));
    
    let svgTexts = document.querySelectorAll('.svgText');
    
    
    (async() => {
      await document.fonts.ready;
      replaceHTMLText(svgTexts)
    })();
    
    
    function replaceHTMLText(svgTexts) {
      svgTexts.forEach(svg => {
        let {
          width
        } = svg.getBBox();
        svg.setAttribute('viewBox', [0, 0, width, 1].join(' '));
      })
    }
    body {
      font-size: 5vmin;
      font-family: georgia, serif;
      padding: 0.5em;
    }
    
    .svgText {
      display: inline-block;
      font-size: 1em;
      height: 1em;
      overflow: visible;
      fill: currentColor;
    }
    
    .resize {
      width: 75%;
      overflow: auto;
      padding: 0.2em;
      outline: 1px solid #ccc;
      resize: both;
    }
    
    p:hover .svgText {
      color: red
    }
    <h3>Hover to see the SVG text elements</h3>
    <div class="resize">
      <p>One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, <svg class="svgText" viewBox="0 0 1 1"><text y="1" font-size="1">slightly</text></svg>    domed and divided by arches into stiff sections. The bedding was <svg class="svgText" viewBox="0 0 1 1"><text y="1" font-size="1">hardly</text></svg> able to cover it and seemed ready to slide off any moment.</p>
    </div>

    Approach 2: Calculate y-offsets via measureText() for absolute position

    We can retrieve some vertical font metrics via measureText() to calculate suitable offsets for the SVG. See also "4.12.5.1.11 Drawing text to the bitmap"

    (async() => {
      await document.fonts.ready;
      positionSVGTextOverlay(htmlText, svgText);
    
    
    })()
    
    function positionSVGTextOverlay(htmlEl, svgEl) {
      // copy styles
      let styles = window.getComputedStyle(htmlEl);
      let {
        fontFamily,
        fontSize
      } = styles;
      fontSize = parseFloat(fontSize);
    
    
      //get HTML element position
      let {
        x,
        y,
        bottom,
        width,
        height
      } = htmlEl.getBoundingClientRect();
    
      // get vertical metrics to calculate y offset
      let metrics = getFontMetrics(fontFamily);
      let {
        fontBoundingBoxAscent,
        fontBoundingBoxDescent,
        hangingBaseline,
        alphabeticBaseline
      } = metrics;
    
      let renderedHeight = (fontBoundingBoxDescent + alphabeticBaseline);
      let scale = 1 + (fontBoundingBoxDescent + alphabeticBaseline) / fontBoundingBoxDescent
      let top = Math.floor(bottom - fontSize * scale)
    
    
      svgEl.setAttribute(
        "style",
        `
      position:absolute;
      display:inline-block;
      font-size: ${fontSize}px;
      font-family: ${fontFamily};
      left: ${x}px;
      top: ${ top  }px;
      `
      );
    
      //adjust viewBox
      let bb = svgEl.getBBox();
      svgEl.setAttribute('viewBox', [0, 0, bb.width, 1].join(' '));
    
    
    }
    
    
    
    function getFontMetrics(font) {
      const canvas = document.createElement("canvas");
      const ctx = canvas.getContext("2d");
      ctx.font = `1000px ${font}`;
      ctx.textBaseline = "top";
      return ctx.measureText('H');
    }
    html,
    body {
      margin: 0;
      padding: 0;
    }
    
    .fnt-siz,
    .tools,
    h3 {
      font-size: 16px;
      line-height: 1.2em;
    }
    
    .tools {
      position: sticky;
      top: 0;
      background: #000;
      color: #fff;
      padding: 0.5em;
      font-size: 20px;
      line-height: 2.5em;
    }
    
    .htmlText {
      font-family: sans-serif, Verdana, Georgia;
      position: absolute;
      background: yellow;
      color: red;
      margin-left: 0;
      padding-left: 0;
      left: 50px;
      top: 200px;
      font-size: 90px;
    }
    
    .svgText {
      font-size: 1em;
      height: 1em;
      overflow: visible;
      outline: 1px solid red;
    }
    <div class="tools">
      <p><label>fontsize <input id="inputFontSize" type="range" value="80" min="10" max="200"></label></p>
    </div>
    
    <h3>Synchonize SVG with HTML font-size</h3>
    <div id="htmlText" class="htmlText">
      First Line
    </div>
    
    <!-- svg text overlay -->
    <svg id="svgText" class="svgText" viewBox="0 0 1 1">
      <text y="1" font-size="1" >First Line
    </text>
    </svg>
    
    <script>
      // fontSize change
      inputFontSize.addEventListener("input", (e) => {
        let fontSize = +e.currentTarget.value;
        htmlText.style.fontSize = `${fontSize}px`;
        positionSVGTextOverlay(htmlText, svgText);
    
      });
    </script>

    However, we still can’t emulate a line breaks.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search