Implementing Text Outline with CSS
CSS doesn’t support text outline out of the box. Here are some ways to create a text outline with CSS.
1. Text Shadow
This method is the most popular way to create a text outline with CSS. It’s simple and is supported by all browsers. However, it can be a bit tricky to get the perfect outline with a thickness of more than 1px. Also, it can suffer from performance issues when used with multiple shadows.
(Open your browser dev tools and see the inline style of the result text below to see how it’s done.)
/* prettier-ignore */
.text-outline {
color: white;
font-weight: 900;
text-shadow:
-1px -1px 0 #000, -1px 0px 0 #000,
-1px 1px 0 #000, 0px -1px 0 #000,
0px 1px 0 #000, 1px -1px 0 #000,
1px 0px 0 #000, 1px 1px 0 #000;
}
The code above will create a text outline with a 1px thickness. You can increase the thickness by adding more shadows.
/* prettier-ignore */
.text-outline {
color: white;
font-weight: 900;
text-shadow:
-2px -2px 0 #000, -2px -1px 0 #000,
-2px 0px 0 #000, -2px 1px 0 #000,
-2px 2px 0 #000, -1px -2px 0 #000,
-1px -1px 0 #000, -1px 0px 0 #000,
-1px 1px 0 #000, -1px 2px 0 #000,
0px -2px 0 #000, 0px -1px 0 #000,
0px 1px 0 #000, 0px 2px 0 #000,
1px -2px 0 #000, 1px -1px 0 #000,
1px 0px 0 #000, 1px 1px 0 #000,
1px 2px 0 #000, 2px -2px 0 #000,
2px -1px 0 #000, 2px 0px 0 #000,
2px 1px 0 #000, 2px 2px 0 #000;
}
Since it can become a little cumbersome, I created a text shadow using JavaScript.
function createTextOutline(thickness: number) {
let shadow = '';
for (let i = -thickness; i <= thickness; i++) {
for (let j = -thickness; j <= thickness; j++) {
if (i === 0 && j === 0) continue;
shadow += `${i}px ${j}px 0 #000,`;
}
}
// remove last comma
return shadow.slice(0, -1);
}
But you might have noticed that the outline looks odd when the thickness is increased. To make it look more natural, the corners of the outline should be rounded. Here’s a better way to create a text outline:
function createTextOutline(thickness: number, resolution: number = 2) {
let shadow = '';
for (let i = -thickness; i <= thickness; i++) {
for (let j = -thickness; j <= thickness; j++) {
if (Math.sqrt(i ** 2 + j ** 2) > thickness || (i === 0 && j === 0)) {
continue;
}
shadow += `${i}px ${j}px 0 #000,`;
}
}
// smooth corners
const arcLength = 1 / resolution;
const angleIncrement = (arcLength / thickness) * (180 / Math.PI);
for (let angle = 0; angle < 360; angle += angleIncrement) {
const radians = (angle * Math.PI) / 180;
const x = Math.cos(radians) * thickness;
const y = Math.sin(radians) * thickness;
if (x % 1 === 0 && y % 1 === 0) continue;
shadow += `${x}px ${y}px 0 #000,`;
}
// remove last comma
return shadow.slice(0, -1);
}
You can increase the resolution for better quality, especially on higher DPR screens. This will create a smoother outline on corners, but performance will suffer.
2. webkit-text-stroke
You might have thought of using the -webkit-text-stroke
property. However, it’s not exactly an outline. It’s a stroke that draws both inside and outside the text. You can get around this by rendering the text twice, once with the stroke and once without it.
Note: Make sure to use twice the thickness you want. This is because the stroke is drawn half inside and half outside the text.
Even though this method is simple and easy to use, it has some limitations:
- There’s a blank space between the text and the stroke when the thickness is increased. I think it’s only chrome that has this issue.
- The corners are not rounded.
- Text overlay doesn’t work properly if the text is not a single line.
- Additionally, see this StackOverflow - Text Stroke (-webkit-text-stroke) css Problem
<style>
.text-stroke {
color: white;
font-weight: 900;
-webkit-text-stroke: 1px black;
}
.text-stroke::before {
content: attr(data-content);
-webkit-text-fill-color: white;
-webkit-text-stroke: 0;
position: absolute;
}
</style>
<div data-content="Text Outline">Text Outline</div>
3. SVG
This method is similar to the -webkit-text-stroke
method but is only supported on modern browsers. Check each property on caniuse for more information.
However, this method also has some limitations.
- There’s a blank space between the text and the stroke when the thickness is increased.
paint-order
is relatively new. You can find alternative ways to achieve the same effect by redrawing the text over the stroke. If you want an implementation example, check out the link in the #See Also section.
Note: Make sure to use a stroke-width of twice the thickness you want. This is because the stroke is drawn half inside and half outside the text.
Another downside of this method is that you have to manually adjust the svg size to fit the text. I haven’t found a way to automatically adjust the svg size without JavaScript. This can be tricky if you have dynamic text. If you have a solution, please let me know.
<style>
text {
fill: white;
font-weight: 900;
stroke: black;
stroke-width: 2px;
stroke-linejoin: round;
paint-order: stroke;
}
</style>
<svg width="120" height="30" viewBox="0 0 120 30">
<text x="10" y="20" fill="white" stroke="black" stroke-width="2">
Text Outline
</text>
</svg>
Quick tip: You can center the text inside svg by setting text-anchor="middle"
and dominant-baseline="central"
, and setting x
and y
to 50%
.
Here’s a simple example of sizing the svg to fit the text:
const svg = document.querySelector('svg');
const text = document.querySelector('text');
const bbox = text.getBBox();
svg.setAttribute('width', bbox.width + 20);
svg.setAttribute('height', bbox.height + 20);