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!
5
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
orgrid
)
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.
1
Doing tests, it seems I found the solution.
If you want to have the SVG <text identical to dom <div text, what you need to do is set:
dominant-baseline=”center” (not middle, not hanging or any other).
Example:
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
<text x="0" y="40px" font-family="verdana" text-anchor="start" font-size="80px" dominant-baseline="central" fill="black">First Line</text>
<text x="0" y="120px" font-family="verdana" text-anchor="start" font-size="80px" dominant-baseline="central" fill="black">Second Line</text>
</svg>
And you need to set the text Y position, to be half the font size
here is a code pen showing the easy solution:
https://codepen.io/Cristian-M/pen/VwJzxmK
@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
Goody Programmer Boy is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
2