06手書き下線のレビュー
レビュー文の一節の下に、ゆがんだSVGの線が左から引かれ、引き終わると消えてまた引かれます。
使いどころレビューカード・本文中の強調
stroke-dashoffsetSVG pathradius 4

<main class="stage">
<figure class="tcol">
<span class="tcol__tape" aria-hidden="true"></span>
<span class="tcol__quote" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M108,72v72H40a8,8,0,0,1-8-8V72a8,8,0,0,1,8-8h60A8,8,0,0,1,108,72Zm108-8H156a8,8,0,0,0-8,8v64a8,8,0,0,0,8,8h68V72A8,8,0,0,0,216,64Z" opacity="0.2"/><path d="M100,56H40A16,16,0,0,0,24,72v64a16,16,0,0,0,16,16h60v8a32,32,0,0,1-32,32,8,8,0,0,0,0,16,48.05,48.05,0,0,0,48-48V72A16,16,0,0,0,100,56Zm0,80H40V72h60ZM216,56H156a16,16,0,0,0-16,16v64a16,16,0,0,0,16,16h60v8a32,32,0,0,1-32,32,8,8,0,0,0,0,16,48.05,48.05,0,0,0,48-48V72A16,16,0,0,0,216,56Zm0,80H156V72h60Z"/></svg>
</span>
<blockquote class="tcol__text">
切り貼りの画面なのに<br>
<span class="tcol__mark">読む順番は揺れない</span>
<svg class="tcol__line" viewBox="0 0 200 12" aria-hidden="true" preserveAspectRatio="none">
<path d="M3 8 C 28 2, 52 11, 78 6 S 132 2, 158 8 S 186 4, 197 6" />
</svg>
</blockquote>
<figcaption class="tcol__by">
<span class="tcol__dot" aria-hidden="true">ジ</span>
<span class="tcol__who">ジョン・雑誌エディター</span>
</figcaption>
</figure>
</main>@use "../../../../site/static/demo-tokens" as *;
// 콜라주·맥시멀 팔레트 (01 과 같은 값)
$cg-cream: #fbf3e4;
$cg-sheet: #ffffff;
$cg-ink: #241d2e;
$cg-slate: #57506a;
$cg-coral: #bb7f74;
$cg-lemon: #d1c8a3;
$cg-lime: #bfce9c;
$serif: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
$duration: $dur-loop; // 선이 다 그어지고 지워지는 한 주기
$easing: $ease-out; // 손으로 긋는 선은 끝에서 느려진다
$color: $cg-coral; // 밑줄 색
$line-len: 230; // path 실측 길이보다 넉넉히 — dasharray·dashoffset 에 같이 쓴다
@include stage($cg-cream);
.tcol {
position: relative;
box-sizing: border-box;
width: clamp(216px, 74vw, 276px);
margin: 0;
padding: $sp-5 $sp-5 $sp-4;
border-radius: $r-xs;
background: $cg-sheet;
box-shadow: 0 6px 0 rgba($cg-ink, .16);
transform: rotate(-1.5deg);
}
// 비뚤게 붙인 마스킹 테이프 한 장
.tcol__tape {
position: absolute;
top: -10px;
left: 50%;
width: 72px;
height: 20px;
margin-left: -36px;
border-radius: 0;
background: rgba($cg-lime, .85);
transform: rotate(-5deg);
}
.tcol__quote {
display: block;
color: $cg-coral;
font-size: 20px;
line-height: 1;
}
.tcol__text {
position: relative;
margin: $sp-2 0 0;
color: $cg-ink;
font-family: $serif;
font-weight: 700;
font-size: clamp(14px, 3.4vw, 17px);
line-height: 1.5;
}
.tcol__mark { position: relative; z-index: 1; }
// 손으로 그은 것 같은 밑줄 — 글자 줄 아래에 딱 붙는다
.tcol__line {
position: absolute;
left: 0;
bottom: -4px;
width: 100%;
height: 12px;
overflow: visible;
fill: none;
stroke: $cg-coral;
stroke-width: 4;
stroke-linecap: round;
stroke-dasharray: $line-len;
animation-name: inkDraw;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.tcol__by {
display: flex;
align-items: center;
gap: $sp-2;
margin-top: $sp-4;
}
.tcol__dot {
width: 24px;
aspect-ratio: 1;
display: grid;
place-items: center;
border-radius: 50%;
background: $cg-lemon;
color: $cg-ink;
font-weight: 700;
font-size: 12px;
line-height: 1;
transform: rotate(6deg);
}
.tcol__who {
color: $cg-slate;
font-weight: 600;
font-size: 12px;
line-height: 1;
}
// 데모: 왼쪽부터 그어졌다가(오프셋 +→0) 왼쪽부터 지워진다(0→-) — 되돌아가는 구간이 없다
@keyframes inkDraw {
0% { stroke-dashoffset: #{$line-len}; }
45% { stroke-dashoffset: 0; }
70% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -#{$line-len}; }
}
@media (max-width: 360px), (max-height: 230px) {
.tcol { width: 208px; padding: $sp-4 $sp-3 $sp-3; }
.tcol__quote { font-size: 15px; }
.tcol__text { font-size: 13px; }
.tcol__by { margin-top: $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.tcol__line { animation: none !important; 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: #fbf3e4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.tcol {
position: relative;
box-sizing: border-box;
width: clamp(216px, 74vw, 276px);
margin: 0;
padding: 20px 20px 16px;
border-radius: 4px;
background: #ffffff;
box-shadow: 0 6px 0 rgba(36, 29, 46, 0.16);
transform: rotate(-1.5deg);
}
.tcol__tape {
position: absolute;
top: -10px;
left: 50%;
width: 72px;
height: 20px;
margin-left: -36px;
border-radius: 0;
background: rgba(191, 206, 156, 0.85);
transform: rotate(-5deg);
}
.tcol__quote {
display: block;
color: #bb7f74;
font-size: 20px;
line-height: 1;
}
.tcol__text {
position: relative;
margin: 8px 0 0;
color: #241d2e;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: clamp(14px, 3.4vw, 17px);
line-height: 1.5;
}
.tcol__mark {
position: relative;
z-index: 1;
}
.tcol__line {
position: absolute;
left: 0;
bottom: -4px;
width: 100%;
height: 12px;
overflow: visible;
fill: none;
stroke: #bb7f74;
stroke-width: 4;
stroke-linecap: round;
stroke-dasharray: 230;
animation-name: inkDraw;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.tcol__by {
display: flex;
align-items: center;
gap: 8px;
margin-top: 16px;
}
.tcol__dot {
width: 24px;
aspect-ratio: 1;
display: grid;
place-items: center;
border-radius: 50%;
background: #d1c8a3;
color: #241d2e;
font-weight: 700;
font-size: 12px;
line-height: 1;
transform: rotate(6deg);
}
.tcol__who {
color: #57506a;
font-weight: 600;
font-size: 12px;
line-height: 1;
}
@keyframes inkDraw {
0% {
stroke-dashoffset: 230;
}
45% {
stroke-dashoffset: 0;
}
70% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -230;
}
}
@media (max-width: 360px), (max-height: 230px) {
.tcol {
width: 208px;
padding: 16px 12px 12px;
}
.tcol__quote {
font-size: 15px;
}
.tcol__text {
font-size: 13px;
}
.tcol__by {
margin-top: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.tcol__line {
animation: none !important;
stroke-dashoffset: 0;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./TestimonialCollage.module.scss";
export type TestimonialCollageProps = {
lead?: string;
/** 밑줄이 그어지는 구절 */
marked?: string;
initial?: string;
who?: string;
quoteIcon?: ReactNode;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TestimonialCollage({
lead = "잘라 붙인 화면인데",
marked = "읽는 순서는 안 흔들려요",
initial = "ㅈ",
who = "정 · 잡지 에디터",
quoteIcon,
duration = 2000,
easing = "ease-out",
color = "#d93a1e",
}: TestimonialCollageProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<figure className={styles.tcol} style={vars}>
<span className={styles.tape} aria-hidden="true" />
<span className={styles.quote} aria-hidden="true">{quoteIcon}</span>
<blockquote className={styles.text}>
{lead}<br />
<span className={styles.mark}>{marked}</span>
<svg className={styles.line} viewBox="0 0 200 12" aria-hidden="true" preserveAspectRatio="none">
<path d="M3 8 C 28 2, 52 11, 78 6 S 132 2, 158 8 S 186 4, 197 6" />
</svg>
</blockquote>
<figcaption className={styles.by}>
<span className={styles.dot} aria-hidden="true">{initial}</span>
<span className={styles.who}>{who}</span>
</figcaption>
</figure>
);
}06 手書き下線のレビュー — 計測 累積面積 0.803% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · コラージュ デザインのLPパーツ9選、コピペOK
このデモが属する記事
コラージュ デザインのLPパーツ9選、コピペOKコラージュ デザインは、余白を取るのではなく色面・大きな活字・シールを重ねて貼り、画面を埋めていくスタイルです。このキットは写真ファイルを1枚も使わず、CSSの値だけで雑誌から切り抜いたような画面を作ります。 この記事のデモをすべて見る


