Text Stroke (-webkit-text-stroke) css Problem
Asked Answered
F

7

12

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 :

enter image description here

Here is the desired behavior :

enter image description here

Code:

<div className="outline-title text-white pb-2 text-5xl font-bold text-center mb-12 mt-8">
      Values &amp; 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.

Browser compatibility: enter image description here

Febricity answered 20/9, 2021 at 11:10 Comment(2)
can you let me know the font which you have used? I have tried in Chrome and Safari, it's working fine codepen.io/pplcallmesatz/pen/oNeyQrvWelty
font-family: "Calibre", "Inter", "San Francisco", "SF Pro Text", -apple-system, system-ui, sans-serif;Febricity
N
8

TL;DR: -webkit-text-stroke is still quite unpredictable

the text-shadow as proposed by @Satheesh Kumar is probably the most reliable solution.

@Luke Taylor's approach – duplicating text to a background pseudo element – also provides a good workaround.

Anatomy of a variable font

As @diopside: pointed out this rendering behaviour is related to variable fonts.
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).

enter image description here

Variable fonts however allow a segemented/overlapping construction of glyphs/characters to facilitate the interpolation between different font weights and widths.

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).

Other issues with -webkit-text-stroke

  • Inconsistent rendering:Firefox renders the stroke with rounded corners
  • weird "kinks and tips" on sharp angles

text-stroke issues

  1. Firefox - Roboto Flex(variable font); 2. Chromium - Roboto Flex(variable font); 3. Chromium - Roboto (static font);

Example snippet: test -webkit-text-stroke rendering

addOutlineTextData();

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"

Needlework answered 12/11, 2021 at 4:5 Comment(0)
W
6

Due to browser compatibility -webkit-text-stroke will not support in a few browsers. You can achieve the outline effect by using shadow.

Hope this works!

.outline-title {
font-family: sans-serif;
   color: white;
   text-shadow:
       1px 1px 0 #000,
     -1px -1px 0 #000,  
      1px -1px 0 #000,
      -1px 1px 0 #000,
       1px 1px 0 #000;
      font-size: 50px;
}
<div class="outline-title text-white pb-2 text-5xl font-bold text-center mb-12 mt-8">
      Values &amp; Process
</div>

---- UPDATE ---

-webkit-text-stroke | MDN Web Docs

Welty answered 10/11, 2021 at 6:22 Comment(2)
-webkit-text-stroke is supported by a lot of browsers, see the edit.Febricity
@Febricity can you please check the updateWelty
S
4

One approach you can take is to cover over the internal lines with a second copy of the text. This produces pretty good results:

Using pseudo-elements, you could do this even without adding a second element to your HTML:

.broken {
  -webkit-text-stroke: 2px black;
}

.fixed {
  position: relative;
  /* We need double the stroke width because half of it gets covered up */
  -webkit-text-stroke: 4px black;
}
/* Place a second copy of the same text over top of the first */
.fixed::after {
  content: attr(data-text);
  position: absolute;
  left: 0;
  -webkit-text-stroke: 0;
  pointer-events: none;
}


div { font-family: 'Inter var'; color: white; }
/* (optional) adjustments to make the two approaches produce more similar shapes */
.broken { font-weight: 800; font-size: 40px; }
.fixed { font-weight: 600; font-size: 39px; letter-spacing: 1.2px; }
<link href="https://rsms.me/inter/inter.css" rel="stylesheet">

Before:
<div class="broken">
  Values &amp; Process
</div>

After:
<div class="fixed" data-text="Values &amp; Process">
  Values &amp; Process
</div>

Note, however, that using a second element is likely better for accessibility than using a pseudo-element, since you can mark it with aria-hidden and ensure screen readers won’t announce the text twice.

A complete example:

.broken {
  -webkit-text-stroke: 2px black;
}

.fixed {
  position: relative;
  /* We need double the stroke width because half of it gets covered up */
  -webkit-text-stroke: 4px black;
}
/* Place the second copy of the text over top of the first */
.fixed span {
  position: absolute;
  left: 0;
  -webkit-text-stroke: 0;
  pointer-events: none;
}


div { font-family: 'Inter var'; color: white; }
/* (optional) adjustments to make the two approaches produce more similar shapes */
.broken { font-weight: 800; font-size: 40px; }
.fixed { font-weight: 600; font-size: 39px; letter-spacing: 1.2px; }
<link href="https://rsms.me/inter/inter.css" rel="stylesheet">

Before:
<div class="broken">
  Values &amp; Process
</div>

After:
<div class="fixed">
  Values &amp; Process
  <span aria-hidden="true">Values &amp; Process</span>
</div>
Sorel answered 28/7, 2022 at 3:35 Comment(4)
I was a little sus of this solution, but it actually works pretty well.Sequent
one issue I found with this is if the data attribute has a quote in it (or any other special characters for that matter), it breaks this. Trying to find a way around this. Might have to just use two elements.Sequent
@Sequent You should be able to escape quotes, see developer.mozilla.org/en-US/docs/Glossary/EntitySorel
This is a cool hack.Wasteful
R
4

I had a similar problem with the 'Nunito' font and this is how I solved it:

  1. Download font editor - https://fontforge.org/en-US/
  2. Open your font in the editor
  3. Select all using Ctrl + A
  4. In the top menu, select Element > Overlap > Union
  5. Then save the new font

This is an example of how the font has changed: enter image description here

This post explains why this bug happens: https://github.com/rsms/inter/issues/292#issuecomment-674993644

Rah answered 27/8, 2023 at 19:53 Comment(1)
Worked perfectly for Public Sans which had the same problem. Note in my version of FontForge, it was Element -> Overlap -> Remove Overlap.Rossierossing
K
3

Its a known issue when using variable-width fonts in certain browsers. As to the why, I have no idea

https://github.com/rsms/inter/issues/292#issuecomment-674993644

Kingcup answered 10/11, 2021 at 9:2 Comment(0)
G
0

This is how I deal with it:

  • Add 2 texts, 1 overlaps another using absolute
  • Wrap them in 1 relative container

Example code (as you are using NextJs and TailwindCSS):

import { ComponentProps } from 'react'

export const TextWithStroke = ({ text, ...props }: ComponentProps<'div'> & { text: string }) => {
      return (
        <div
          {...props}
          style={{
            position: 'relative',
            ...props.style
          }}>
          <p className="text-stroke">{text}</p>
          <p className="top-0 absolute">{text}</p>
        </div>
      )
    }

And text-stroke means -webkit-text-stroke, I have defined in a global css file, like this:

@layer utilities {
    .text-stroke {
        -webkit-text-stroke: 5px #4DDE4D;
    }
}

This is the result:
This is the result

Genovevagenre answered 5/4 at 15:15 Comment(0)
M
-1

I had the same problem before, It turns out that I've initialized 'Montserrat' as my primary font and applied Some other font to an element. But when I changed the font from 'Montserrat' to 'Poppins' the problem was solved :P

Moil answered 3/8, 2022 at 19:0 Comment(1)
That's not a good answer, honestly. I'd like to keep my current font.Requisition

© 2022 - 2024 — McMap. All rights reserved.