I am working on a personal project with NextJs and TailwindCSS.
upon finishing the project I used a private navigator to see my progress, but it seems that the stroke is not working as it should, I encounter this in all browsers except Chrome.
Here is what i get :
Here is the desired behavior :
Code:
<div className="outline-title text-white pb-2 text-5xl font-bold text-center mb-12 mt-8">
Values & Process
</div>
Css:
.outline-title {
color: rgba(0, 0, 0, 0);
-webkit-text-stroke: 2px black;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
Can someone explain or help to fix this.
-webkit-text-stroke
doesn't work well with variable fontspaint-order
to HTML textAll credits to HyoukJoon Lee's answer here: "CSS Font Border?". Admittedly, it is not quite clear from the W3C specs why we can apply the SVG paint-order
property to HTML text elements as well as described on mdn docs. We won't need duplicated text via pseudo-elements.
All major rendering engines (Firefox/gecko, Chromium/blink, Safari/webkit) seem to support this property flawlessly.
@font-face {
font-family: 'Roboto Flex';
font-style: normal;
font-weight: 100 1000;
font-stretch: 0% 200%;
src: url(https://fonts.gstatic.com/s/robotoflex/v9/NaNeepOXO_NexZs0b5QrzlOHb8wCikXpYqmZsWI-__OGfttPZktqc2VdZ80KvCLZaPcSBZtOx2MifRuWR28sPJtUMbsFEK6cRrleUx9Xgbm3WLHa_F4Ep4Fm0PN19Ik5Dntczx0wZGzhPlL1YNMYKbv9_1IQXOw7AiUJVXpRJ6cXW4O8TNGoXjC79QRyaLshNDUf9-EmFw.woff2) format('woff2');
}
body {
font-family: 'Roboto Flex';
font-weight: 500;
font-size: 10vmin;
margin: 2em;
background-color: #999;
}
h3 {
font-size: 16px;
color: #fff
}
h1 {
-webkit-text-stroke: 0.02em black;
color: #fff;
font-stretch: 0%;
font-weight: 200;
}
/* render stroke behind text-fill color */
.outline {
-webkit-text-stroke: 0.04em black;
paint-order: stroke fill;
}
<h3>Stroked - showing geometry of the font</h3>
<h1>AVATAR</h1>
<h3>Outlined - stroke behind fill</h3>
<h1 class="outline">AVATAR</h1>
The reason for these inner outlines is based on the structure of some variable fonts.
'Traditional' fonts (so before variable fonts) – only contained an outline shape and maybe a counter shape e.g the cut out inner 'hole' of a lowercase e glyph.
Otherwise you would have encountered undesired even/odd issues resulting in excluded shapes caused by overlapping path areas.
Applying this construction method, you will never see any overlap of shapes. You could imagine them as rather 'merged down' compound paths. Counter shapes like the aforementioned hole were based on simple rules like a counterclockwise path directions – btw. you might still encounter this concept in svg-clipping paths - not perfectly rendering in some browsers).
Variable fonts however allow a segemented/overlapping construction of glyphs/characters to facilitate the interpolation between different font weights and widths. See also "Microsoft typography: Comparison of 'glyf', 'CFF ' and CFF2 tables"
Obviously webkit-text-stroke
outlines the exact bézier anatomy of a glyph/character resulting in undesired outlines for every glyph component.
This is not per se an issue of variable fonts, since weight and width interpolations has been used in type design for at least 25 years. So this quirky rendering issue depends on the used font – a lot of classic/older fonts compiled to the newer variable font format will still rely on the old school aproach (avoiding any overlap).
In your case you can still find alternative non-variable versions of this font e.g on github or google web fonts helper
/* latin */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
src: url(https://fonts.gstatic.com/s/inter/v18/UcCo3FwrK3iLTcviYwYZ90A2N58.woff2)
format("woff2");
}
@font-face {
font-family: 'InterStatic';
font-style: normal;
font-weight: 700;
src: url(https://cdn.jsdelivr.net/gh/rsms/inter@master/docs/font-files/InterDisplay-Bold.woff2) format('woff2');
}
body {
font-family: "Inter";
font-size: 5em;
color: #fff;
-webkit-text-stroke: 0.02em red;
}
.interStatic{
font-family: 'InterStatic';
}
<h1>Values & Process</h1>
<h1 class="interStatic">Values & Process</h1>
-webkit-text-stroke
-webkit-text-stroke
renderingaddOutlineTextData();
function addOutlineTextData() {
let textOutline = document.querySelectorAll(".textOutlined");
textOutline.forEach((text) => {
text.dataset.content = text.textContent;
});
}
let root = document.querySelector(':root');
sampleText.addEventListener("input", (e) => {
let sampleText = e.currentTarget.textContent;
let textOutline = document.querySelectorAll(".textOutlined");
textOutline.forEach((text) => {
text.textContent = sampleText;
text.dataset.content = sampleText;
});
});
strokeWidth.addEventListener("input", (e) => {
let width = +e.currentTarget.value;
strokeWidthVal.textContent = width + 'em'
root.style.setProperty("--strokeWidth", width + "em");
});
fontWeight.addEventListener("input", (e) => {
let weight = +e.currentTarget.value;
fontWeightVal.textContent = weight;
document.body.style.fontWeight = weight;
});
useStatic.addEventListener("input", (e) => {
let useNonVF = useStatic.checked ? true : false;
if (useNonVF) {
document.body.style.fontFamily = 'Roboto';
} else {
document.body.style.fontFamily = 'Roboto Flex';
}
});
@font-face {
font-family: 'Roboto Flex';
font-style: normal;
font-weight: 100 1000;
font-stretch: 0% 200%;
src: url(https://fonts.gstatic.com/s/robotoflex/v9/NaNeepOXO_NexZs0b5QrzlOHb8wCikXpYqmZsWI-__OGfttPZktqc2VdZ80KvCLZaPcSBZtOx2MifRuWR28sPJtUMbsFEK6cRrleUx9Xgbm3WLHa_F4Ep4Fm0PN19Ik5Dntczx0wZGzhPlL1YNMYKbv9_1IQXOw7AiUJVXpRJ6cXW4O8TNGoXjC79QRyaLshNDUf9-EmFw.woff2) format('woff2');
}
body {
font-family: 'Roboto Flex';
font-weight: 500;
margin: 2em;
}
.p,
p {
margin: 0;
font-size: 10vw;
}
.label {
font-weight: 500!important;
font-size: 15px;
}
.resize {
resize: both;
border: 1px solid #ccc;
overflow: auto;
padding: 1em;
width: 40%;
}
:root {
--textOutline: #000;
--strokeWidth: 0.1em;
}
.stroke {
-webkit-text-stroke: var(--strokeWidth) var(--textOutline);
color: #fff
}
.textOutlined {
position: relative;
color: #fff;
}
.textOutlined:before {
content: attr(data-content);
position: absolute;
z-index: -1;
color: #fff;
top: 0;
left: 0;
-webkit-text-stroke: var(--strokeWidth) var(--textOutline);
display: block;
width: 100%;
}
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700;900" rel="stylesheet">
<p class="label">stroke width<input id="strokeWidth" type="range" value="0.3" min='0.01' max="0.5" step="0.001"><span id="strokeWidthVal">0.25em</span> | font-weight<input id="fontWeight" type="range" value="100" min='100' max="900" step="10"><span id="fontWeightVal">100</span>
<label><input id="useStatic" type="checkbox">Use static Roboto</label><br><br>
</p>
<div id="sampleText" class="stroke p" contenteditable>AVATAR last <br>Airbender</div>
<p class="label">Outline via pseudo element in background</p>
<div class="resize">
<p class="textOutlined">AVATAR last Airbender
</p>
</div>
However, these rendering issues are rare as long as your stroke-width is not significantly larger than ~0.1em (or 10% of your current font-size).
See also "Outline effect to text"
We're basically querying specified HTML elements and rebuild them as SVG <text>
elements. The main advantage of this approach: we have more fine-grained control over the text-stroke rendering due to SVGs paint-order
attribute – update:this also works for HTML elements. Besides, we get a more predictable corner rounding via stroke-linecap
and stroke-linejoin
:
btnConvert.onclick = () => {
htmlText2SvgText();
}
/* when loaded instantly - wait for all fonts to be loaded
(async () => {
await document.fonts.ready;
htmlText2SvgText();
})();
*/
function htmlText2SvgText(selector = ".html2SvgText") {
let textEls = document.querySelectorAll(selector);
// quit if already converted
let processedEls = document.querySelectorAll('.svgTxt');
if (processedEls.length) return;
textEls.forEach(textEl => {
// get text nodes
let textNodes = getTextNodesInEL(textEl);
textNodes.forEach(textNode => {
let textParent = textNode.parentElement;
// split to words to ensure line wrapping
let words = textNode.textContent.split(' ').filter(Boolean);
// get font style properties from parent
let style = window.getComputedStyle(textParent)
let {
webkitTextStrokeWidth,
webkitTextStrokeColor,
fontSize,
fontStretch,
fontStyle,
color,
letterSpacing,
wordSpacing,
} = style;
/**
* convert property values
* to relative em based values
* used for SVG text conversion
*/
let strokeWidthRel = Math.ceil(100 / parseFloat(fontSize) * parseFloat(webkitTextStrokeWidth) * 2)
let letterSpacingRel = letterSpacing && letterSpacing !== 'normal' ? (parseFloat(letterSpacing) / parseFloat(fontSize)).toFixed(3) : 0;
let wordSpacingRel = wordSpacing && wordSpacing != 'normal' ? +(parseFloat(wordSpacing) / parseFloat(fontSize)).toFixed(3) : 0;
// adjust letter and word spacing for parent element
if (letterSpacingRel || wordSpacingRel) textParent.setAttribute('style', `letter-spacing:0em; word-spacing:${(letterSpacingRel) * words.length + wordSpacingRel}em`);
// loop words and replace them with SVG
words.forEach((word, i) => {
// add space in between word SVGs
let space = i < words.length - 1 ? ' ' : '';
let svg = new DOMParser().parseFromString(
`<svg class="svgTxt" viewBox="0 0 100 100" style="overflow:visible; display:inline-block;height:1em; width:auto;line-height:1em;margin-top: -100px;">
<text class="svgTxt-text" x="0" y="100"
font-size="100"
fill="currentColor"
style="font-kerning:normal; font-stretch: ${fontStretch}"
stroke="${webkitTextStrokeColor}"
stroke-width="${strokeWidthRel}"
letter-spacing="${letterSpacingRel}em"
paint-order="stroke"
stroke-linecap="round"
stroke-linejoin="round">${word.trim()}</text>
</svg>`,
'text/html'
).querySelector('svg');
//textParent.insertAdjacentHTML('afterbegin', svg);
textParent.insertBefore(svg, textNode);
if (i < words.length - 1) {}
// add spaces
let spaceNode = document.createTextNode(' ');
textParent.insertBefore(spaceNode, svg.nextSibling);
//let svgEls = textParent.querySelectorAll('svg');
let textSVG = svg.querySelector('text')
//get bbox
let {
x,
width
} = textSVG.getBBox();
// shorten by letter spacing value
let shorten = 100 * letterSpacingRel;
shorten = letterSpacingRel + wordSpacingRel * 2;
svg.setAttribute('viewBox', [Math.floor(x), 0, Math.floor(width) - shorten, 100].join(' '));
({
x,
width
} = textSVG.getBBox());
svg.setAttribute('viewBox', [Math.floor(x), 0, Math.floor(width) - shorten, 100].join(' '));
})
// erase currenet text content
textNode.remove();
})
})
}
// text helpers
function getTextNodesInEL(el) {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
const nodes = [];
while (walker.nextNode()) {
nodes.push(walker.currentNode);
}
return nodes;
}
* {
box-sizing: border-box;
}
@font-face {
font-family: 'Roboto Flex';
font-style: oblique 0deg 10deg;
font-weight: 100 1000;
font-stretch: 25% 151%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/robotoflex/v26/NaPccZLOBv5T3oB7Cb4i0zu6RME.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* prevent faux italicizing */
em {
font-variation-settings: 'slnt' -10;
font-style: normal;
}
body {
font-family: "Roboto", sans-serif;
font-family: "Roboto Flex", sans-serif;
}
.resize {
font-size: 5vw;
letter-spacing: 0.01em;
font-stretch: 110%;
overflow: auto;
padding: 0.1em;
border: 1px solid #ccc;
width: 100%;
resize: both;
}
h1 {
font-size: 3em;
line-height: 1.1em;
font-weight: 400;
font-stretch: 30%;
text-transform: uppercase;
letter-spacing: 0;
margin: 0;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
strong {
font-stretch: 150%;
}
.stroked-text {
-webkit-text-stroke: 2px darkred;
color: #fff;
}
<p>
<button id="btnConvert">convert HTML els to SVG</button>
</p>
<div class="resize">
<h1 class="html2SvgText stroked-text">Franz Kafka <span style="-webkit-text-stroke-color:green; -webkit-text-stroke-width:3px;font-stretch:75%">The
Metamorphosis</span></h1>
<p>One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a
<strong class="html2SvgText"><em><span class="stroked-text">horrible vermin.</span></em></strong> He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections.
The bedding was hardly able to cover it and seemed ready to slide off any moment.
</p>
</div>
letter-spacing
) wont't auto update as we would need to recalculate the SVGs' viewBox
again