03Letters Whose Eight-direction Values Merge Into an Outline
Shadows are placed at the same distance value in eight directions (up, down, left, right, and the four diagonals), so only an outline remains around the letters with no fill.
Best forEvent poster numerals
OUTLINE_MAX_PXbuildOutlineShadowrenderOutline

<main class="stage">
<div class="ol is-demo" id="ol">
<p class="ol__badge"><span class="ol__badgeLabel">Outline width</span> <span class="ol__badgeValue" id="olValue">0.5px</span></p>
<h1 class="ol__word" id="olWord">Outline</h1>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 핵심: 값 d 하나를 여덟 방향(상하좌우+대각선)에 같은 거리만큼 겹쳐, 글자 채움 없이도
// 둘레에 윤곽선만 남는다. d 가 커질수록 윤곽선이 두꺼워진다.
$duration: $dur-loop;
$easing: linear;
$color: $stage-orange;
@include stage($stage-orange);
.ol {
display: grid;
gap: $sp-4;
justify-items: center;
text-align: center;
}
.ol__badge {
margin: 0;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: rgba(23, 20, 26, .12);
color: $subject-ink;
font-weight: 700;
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.ol__word {
margin: 0;
font-weight: 800;
font-size: clamp(72px, 24vw, 140px);
line-height: 1;
white-space: nowrap;
color: $color;
will-change: text-shadow;
}
@media (max-width: 340px) {
.ol__word { font-size: clamp(52px, 26vw, 92px); }
}
@media (prefers-reduced-motion: reduce) {
.ol__word { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ol {
display: grid;
gap: 16px;
justify-items: center;
text-align: center;
}
.ol__badge {
margin: 0;
padding: 4px 12px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.12);
color: #17141a;
font-weight: 700;
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.ol__word {
margin: 0;
font-weight: 800;
font-size: clamp(72px, 24vw, 140px);
line-height: 1;
white-space: nowrap;
color: #ff4d1f;
will-change: text-shadow;
}
@media (max-width: 340px) {
.ol__word {
font-size: clamp(52px, 26vw, 92px);
}
}
@media (prefers-reduced-motion: reduce) {
.ol__word {
transition: none;
}
}(function () {
var OUTLINE_MAX_PX = 15;
var PERIOD_MS = 1000;
var DIRS = [
[1, 0], [-1, 0], [0, 1], [0, -1],
[0.7071, 0.7071], [-0.7071, 0.7071], [0.7071, -0.7071], [-0.7071, -0.7071]
];
var word = document.getElementById('olWord');
var value = document.getElementById('olValue');
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 값(d) 하나를 여덟 방향에 같은 거리만큼 겹쳐 채움 없이도 윤곽선만 남긴다
function buildOutlineShadow(d) {
var parts = [];
for (var i = 0; i < DIRS.length; i++) {
var x = (DIRS[i][0] * d).toFixed(2);
var y = (DIRS[i][1] * d).toFixed(2);
parts.push(x + 'px ' + y + 'px 0 rgba(23,20,26,.92)');
}
return parts.join(', ');
}
function renderOutline(d) {
word.style.textShadow = buildOutlineShadow(d);
value.textContent = d.toFixed(1) + 'px';
}
var startTime = null;
function loop(now) {
if (startTime === null) startTime = now;
var t = (now - startTime) % PERIOD_MS;
var phase = (Math.sin((t / PERIOD_MS) * Math.PI * 2 - Math.PI / 2) + 1) / 2;
renderOutline(0.4 + (OUTLINE_MAX_PX - 0.4) * phase);
requestAnimationFrame(loop);
}
if (reduce) {
renderOutline(OUTLINE_MAX_PX);
} else {
requestAnimationFrame(loop);
}
})();// TextShadowStrokeOutlineMulti — vanilla 판(run/412/demo/03-text-shadow-stroke-outline-multi/index.html) 의 React 포트.
// 값 d 하나를 여덟 방향(상하좌우+대각선)에 같은 거리만큼 겹쳐, 글자 채움 없이도
// 둘레에 윤곽선만 남는다. d 가 커질수록 윤곽선이 두꺼워진다.
import { useEffect, useRef, useState } from "react";
import styles from "./TextShadowStrokeOutlineMulti.module.scss";
type Box<T> = { current: T };
const OUTLINE_MAX_PX = 15;
const PERIOD_MS = 1000;
const DIRS: [number, number][] = [
[1, 0], [-1, 0], [0, 1], [0, -1],
[0.7071, 0.7071], [-0.7071, 0.7071], [0.7071, -0.7071], [-0.7071, -0.7071]
];
function buildOutlineShadow(d: number): string {
const parts: string[] = [];
for (let i = 0; i < DIRS.length; i++) {
const x = (DIRS[i][0] * d).toFixed(2);
const y = (DIRS[i][1] * d).toFixed(2);
parts.push(x + "px " + y + "px 0 rgba(23,20,26,.92)");
}
return parts.join(", ");
}
export default function TextShadowStrokeOutlineMulti() {
const wordRef = useRef<HTMLHeadingElement | null>(null);
const valueRef = useRef<HTMLSpanElement | null>(null);
const t0Ref = useRef<number>(0) as Box<number>;
const frameRef = useRef<number>(0) as Box<number>;
const [demo] = useState(true);
const [reduce, setReduce] = useState(false);
function render(d: number): void {
const word = wordRef.current;
const value = valueRef.current;
if (!word || !value) return;
word.style.textShadow = buildOutlineShadow(d);
value.textContent = d.toFixed(1) + "px";
}
useEffect(() => {
setReduce(window.matchMedia("(prefers-reduced-motion: reduce)").matches);
}, []);
useEffect(() => {
if (reduce) {
render(OUTLINE_MAX_PX);
return;
}
function loop(now: number): void {
if (t0Ref.current === 0) t0Ref.current = now;
const t = (now - t0Ref.current) % PERIOD_MS;
const phase = (Math.sin((t / PERIOD_MS) * Math.PI * 2 - Math.PI / 2) + 1) / 2;
render(0.4 + (OUTLINE_MAX_PX - 0.4) * phase);
frameRef.current = requestAnimationFrame(loop);
}
frameRef.current = requestAnimationFrame(loop);
return () => cancelAnimationFrame(frameRef.current);
}, [reduce]);
const cls = [styles.ol, demo ? styles.isDemo : ""].filter(Boolean).join(" ");
return (
<main className={styles.stage}>
<div className={cls}>
<p className={styles.badge}><span>윤곽 두께</span> <span ref={valueRef}>0.5px</span></p>
<h1 className={styles.word} ref={wordRef}>윤곽</h1>
</div>
</main>
);
}03 Letters Whose Eight-direction Values Merge Into an Outline — Measured area 10.6% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · CSS Text Shadow Effects — 9 Patterns
The post this demo lives in
CSS Text Shadow Effects — 9 Patternscss text shadow effects: A shadow is where light goes missing, yet these nine letters shine by shadow alone — item 1 sta See all demos in this post


