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

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/oNeyQrv Commented Nov 10, 2021 at 5:09
  • font-family: "Calibre", "Inter", "San Francisco", "SF Pro Text", -apple-system, system-ui, sans-serif;
    – agoumi
    Commented Nov 10, 2021 at 8:32

7 Answers 7

12
+50

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"

0
9

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,
      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

4
  • -webkit-text-stroke is supported by a lot of browsers, see the edit.
    – agoumi
    Commented Nov 10, 2021 at 8:42
  • @agoumi can you please check the update Commented Nov 11, 2021 at 11:14
  • Thanks for the solution! I believe the last 1px 1px 0 #000; is redundant?
    – Xitang
    Commented Jun 29 at 18:41
  • 1
    Hi @Xitang , apologies for the mistake. Please ignore the last line. I will update the answer accordingly. Thanks for pointing it out! Commented Jul 1 at 6:58
5

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>

4
  • I was a little sus of this solution, but it actually works pretty well.
    – CRAIG
    Commented Dec 26, 2022 at 15:13
  • 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.
    – CRAIG
    Commented Jan 4, 2023 at 22:43
  • 1
    @CRAIG You should be able to escape quotes, see developer.mozilla.org/en-US/docs/Glossary/Entity Commented Jan 5, 2023 at 20:45
  • This is a cool hack.
    – Deepak
    Commented Aug 14, 2023 at 7:56
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

1
  • Worked perfectly for Public Sans which had the same problem. Note in my version of FontForge, it was Element -> Overlap -> Remove Overlap.
    – Zwei
    Commented Sep 9, 2023 at 1:09
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

1

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

0

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

1
  • 1
    That's not a good answer, honestly. I'd like to keep my current font.
    – msi
    Commented Aug 19, 2023 at 12:59

Not the answer you're looking for? Browse other questions tagged or ask your own question.