Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simulate external stroke ::before pseudo elements: problem with transparent text

I find it difficult to believe that there is no standard and simple (and browser-independent) way to put a stroke effect around the outside of text using CSS.

We do have -webkit-text-stroke but for some odd reason the stroke is centred around the border of the text rather than outside it, as bemoaned here.

So I'm trying to implement a workaround based on this idea, which places the stroked text in a pseudo element behind the original un-stroked text. I've demonstrated this in this jsfiddle, with the following code:

var jQueryAttr = function(selector, attr, setterFunction) {
  document.querySelectorAll(selector).forEach((el, i) => {
    el.setAttribute(attr, setterFunction.call(el, i, attr));
  });
};

jQueryAttr('.myclass', 'data-myclass', function(index, attr) {
  return this.innerHTML;
});
body {
  background: none;
}

.basic {
  color: rgba(186, 218, 85, 1);
  font: 2.5em Georgia, serif;
}

.myclass {
  position: relative;
  background: transparent;
  z-index: 0;
}

.myclass::before {
  content: attr(data-myclass);
  position: absolute;
  -webkit-text-stroke: 0.2em rgba(0, 0, 0, 1);
  z-index: -1;
}

.anotherclass {
  -webkit-text-stroke: 0.2em rgba(0, 0, 0, 1);
}
<p class="basic">Text without any stroke</p>
<p class="myclass basic">Text with outer stroke</p>
<p class="anotherclass basic">Without the trick applied</p>

This works fine, except that if the text itself has some transparency then you see the dark stroke underneath, as shown in this variant (the only change is to peg the opacity of the text back to 0.3). As you can see, the black from the stroked element is leaching through into the text (in the top line).

So is there another neat trick to use to overcome this problem? I guess it's possible to add another pseudo element between the stroked layer and the un-stroked layer, with a pure white text (or one to match the background), but I'd like to apply this technique in a context where I don't know the colour of the background in advance... e.g. where this is laid over an arbitrary user-selected image. For this reason, I've set the background of the body to none in the above example.

like image 772
drmrbrewer Avatar asked Mar 05 '19 09:03

drmrbrewer


Video Answer


2 Answers

Here is an idea where you can consider mix-blend-mode and a combination of text-shadow to approximate this. The tricky part is to adjust the shadow in case you want a bigger stroke:

.text > span {
  font-family:sans-serif;
  font-size:60px;
  font-weight: bold;
  color:#fff; /*use white*/
  /*create the stroke around text*/
  text-shadow:
    2px 0  0px #000,
    0 2px 0px #000,
    2px 2px 0px #000,
    -2px 0 0px #000,
    0 -2px 0px #000,
    -2px -2px 0px #000,
    -2px 2px 0px #000,
    2px -2px 0px #000;
  mix-blend-mode: darken; /*everything is more dark than white so we always see the background */
}
.text {
  display:inline-block;
  padding:20px;
  background:linear-gradient(to right,red, blue);
}
<div class="text"><span>Some text here</span></div>

Using CSS variable will probably make it easier to adjust:

.text > span {
  font-family:sans-serif;
  font-size:60px;
  font-weight: bold;
  color:#fff; /*use white*/
  /*create the stroke around text*/
  text-shadow:
    var(--s,2px) 0  var(--c,0) #000,
    0 var(--s,2px) var(--c,0) #000,
    var(--s,2px) var(--s,2px) var(--c,0) #000,
    calc(-1*var(--s,2px)) 0 var(--c,0) #000,
    0 calc(-1*var(--s,2px)) var(--c,0) #000,
    calc(-1*var(--s,2px)) calc(-1*var(--s,2px)) var(--c,0) #000,
    calc(-1*var(--s,2px)) var(--s,2px) var(--c,0) #000,
    var(--s,2px) calc(-1*var(--s,2px)) var(--c,0) #000;
  mix-blend-mode: darken; /*everything is more dark than white so we always see the background */
}
.text {
  display:inline-block;
  padding:20px;
  background:linear-gradient(to right,red, blue);
  background-size:cover;
  background-position:center;
}
<div class="text"><span>Some text here</span></div>

<div class="text" style="--s:4px;--c:2px;background-image:url(https://picsum.photos/800/600?image=1069)"><span>Some text here</span></div>

<div class="text" style="--s:6px;--c:4px;background-image:url(https://picsum.photos/800/600?image=1051)"><span>Some text here</span></div>

If you want a transparent color for the text you can duplicate it using pseudo element:

.text > span {
  font-family:sans-serif;
  font-size:60px;
  font-weight: bold;
  position:relative;
  display:inline-block;
}
.text > span::before,
.text > span::after {
  content:attr(data-text);
}
.text > span::before {
  color:#fff; /*use white*/
  /*create the stroke around text*/
  text-shadow:
    var(--s,2px) 0  var(--c,0) #000,
    0 var(--s,2px) var(--c,0) #000,
    var(--s,2px) var(--s,2px) var(--c,0) #000,
    calc(-1*var(--s,2px)) 0 var(--c,0) #000,
    0 calc(-1*var(--s,2px)) var(--c,0) #000,
    calc(-1*var(--s,2px)) calc(-1*var(--s,2px)) var(--c,0) #000,
    calc(-1*var(--s,2px)) var(--s,2px) var(--c,0) #000,
    var(--s,2px) calc(-1*var(--s,2px)) var(--c,0) #000;
  mix-blend-mode: darken; /*everything is more dark than white so we always see the background */
}
.text > span::after {
  position:absolute;
  top:0;
  left:0;
  color:rgba(0,255,0,0.4); 
}
.text {
  display:inline-block;
  padding:20px;
  background:linear-gradient(to right,red, blue);
  background-size:cover;
  background-position:center;
}
<div class="text"><span data-text="Some text here"></span></div>

<div class="text" style="--s:4px;--c:2px;background-image:url(https://picsum.photos/800/600?image=1069)"><span data-text="Some text here"></span></div>

<div class="text" style="--s:6px;--c:4px;background-image:url(https://picsum.photos/800/600?image=1051)"><span data-text="Some text here"></span></div>
like image 133
Temani Afif Avatar answered Oct 26 '22 08:10

Temani Afif


The easiest, and with best browser support might actually be SVG.
You can set up approximately the same thing you did with the ::before, with the difference that the background stroked version can have a mask, which will let only the outer-line visible.
From there, you can simply append a copy of the same text over, and you'll be able to apply the opacity as you wish, both on the stroke and the fill:

body{
  background-image:url(https://picsum.photos/800/200?image=1051);
  font-family: sans-serif;
}
svg {
  font-size: 40px;
  font-weight: bold;
}
.textStroke {
  stroke: black;
  stroke-width: 12px;
  stroke-linejoin: round;
}
.visibleText {
  fill: rgba(186, 218, 85, 1);
  transition: fill-opacity .5s linear;
}
.visibleText:hover {
  fill-opacity: 0;
}
<svg width="350">
  <defs>
    <!-- we type it only once -->
    <text x="10" y="55" id="txt">Text with outline</text>
    <mask id="mymask">
      <!-- white => visible, black => tansparent -->
      <rect x="0" y="0" width="450" height="70" fill="#FFF"></rect>
      <use xlink:href="#txt" fill="#000"/>
    </mask>
  </defs>
  <!-- our stroked text, with the mask -->
  <use xlink:href="#txt" mask="url(#mymask)" class="textStroke"/>
  <!-- fill version -->
  <use xlink:href="#txt" class="visibleText"/>
</svg>
like image 36
Kaiido Avatar answered Oct 26 '22 07:10

Kaiido