03Hand-drawn underline
An inline SVG path draws a slightly uneven curve; stroke-dasharray is set to the path's full length and stroke-dashoffset animates from that value to 0, so the underline appears to be drawn by hand left to right.
Best forA keyword inside a blog pull-quote, an emphasized word on an about page
SVG pathstroke-dashoffset600ms

<main class="stage">
<p class="quote">
“<span class="hl">Consistency</span> beats skill.”
<svg class="hl__svg" viewBox="0 0 120 20" preserveAspectRatio="none" aria-hidden="true">
<path class="hl__path" d="M2 12 C 20 18, 45 6, 62 11 S 100 17, 118 9" />
</svg>
</p>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ─────────────────────────────
$duration: 600ms; // 손으로 한 획을 긋는 데 걸리는 시간
$easing: $ease-out;
$color: $subject-blue; // 밑줄 색
// ── 데모 무대 ───────────────────────────────
@include stage($stage-ink);
.quote {
position: relative;
display: inline-block;
margin: 0;
color: $subject-cream;
font-weight: 600;
font-size: clamp(17px, 4.8vw, 23px);
line-height: 1.7;
font-family: inherit;
text-align: center;
}
.hl {
position: relative;
color: $subject-cream;
}
// 인용구 바로 아래, 키워드 폭만큼 걸쳐지는 SVG 밑줄 — 살짝 삐뚤한 손글씨 곡선
.hl__svg {
position: absolute;
left: 6%;
right: 46%; // "꾸준함" 위치에 맞춘 대략적 폭 (데모 전용 좌표)
bottom: -6px;
height: 16px;
overflow: visible;
}
.hl__path {
fill: none;
stroke: $color;
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 140;
stroke-dashoffset: 140;
animation: draw $duration $easing infinite, draw-thick $duration ease-in-out infinite;
animation-delay: .2s;
}
@keyframes draw {
0% { stroke-dashoffset: 140; }
35% { stroke-dashoffset: 0; }
80% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -140; }
}
// 한 겹: 그어지는 동안 펜 압력처럼 stroke-width 가 요동 — stroke-width 는 transform 이
// 아니라 draw(stroke-dashoffset) 와 병렬로 돌아도 서로 덮지 않는다
@keyframes draw-thick {
0% { stroke-width: 3px; }
25% { stroke-width: 4.5px; }
45% { stroke-width: 3.5px; }
65% { stroke-width: 4px; }
85% { stroke-width: 3.2px; }
100% { stroke-width: 3px; }
}
@media (prefers-reduced-motion: reduce) {
.hl__path { animation: none; stroke-dashoffset: 0; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.quote {
position: relative;
display: inline-block;
margin: 0;
color: #fff7e6;
font-weight: 600;
font-size: clamp(17px, 4.8vw, 23px);
line-height: 1.7;
font-family: inherit;
text-align: center;
}
.hl {
position: relative;
color: #fff7e6;
}
.hl__svg {
position: absolute;
left: 6%;
right: 46%;
bottom: -6px;
height: 16px;
overflow: visible;
}
.hl__path {
fill: none;
stroke: #2f6df6;
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 140;
stroke-dashoffset: 140;
animation: draw 600ms ease-out infinite, draw-thick 600ms ease-in-out infinite;
animation-delay: 0.2s;
}
@keyframes draw {
0% {
stroke-dashoffset: 140;
}
35% {
stroke-dashoffset: 0;
}
80% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -140;
}
}
@keyframes draw-thick {
0% {
stroke-width: 3px;
}
25% {
stroke-width: 4.5px;
}
45% {
stroke-width: 3.5px;
}
65% {
stroke-width: 4px;
}
85% {
stroke-width: 3.2px;
}
100% {
stroke-width: 3px;
}
}
@media (prefers-reduced-motion: reduce) {
.hl__path {
animation: none;
stroke-dashoffset: 0;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./HandwrittenUnderline.module.scss";
export type HandwrittenUnderlineProps = {
/** 밑줄이 걸리는 강조어 */
keyword: ReactNode;
/** 강조어 뒤 문장 */
after?: ReactNode;
/** 한 획을 긋는 데 걸리는 시간 (기본 600ms) */
duration?: number;
easing?: string;
/** 밑줄 색 */
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
export default function HandwrittenUnderline({
keyword,
after,
duration = 600,
easing = "ease-out",
color = "#2f6df6",
}: HandwrittenUnderlineProps) {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<p className={styles.quote} style={vars}>
“<span className={styles.hl}>{keyword}</span>{after}
<svg className={styles.svg} viewBox="0 0 120 20" preserveAspectRatio="none" aria-hidden="true">
<path className={styles.path} d="M2 12 C 20 18, 45 6, 62 11 S 100 17, 118 9" />
</svg>
</p>
);
}03 Hand-drawn underline — Measured area 0.301% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · 9 CSS Text Highlight Animations, Copy-Paste
The post this demo lives in
9 CSS Text Highlight Animations, Copy-PasteA css text highlight animation calls attention to one word or line with a highlighter stroke, an underline, or a striket See all demos in this post


