ペーパーカット デザインのLPパーツ9選、コピペOK
ペーパーカット デザインは、はさみで切って貼ったような面をぼかし0の影だけで立ち上げ、色は決められた5色の中から選ぶスタイルです。紙のテクスチャ画像を1枚も敷かず、影の値ひとつで紙らしさを出します。
Pinterestで「paper cutout」として保存されている作例の多くはPhotoshopで作った画像なので、そのままWebに持ち込むと重くなります。ここでは画像を1枚も使わず、CSSの値だけで同じ印象を出しました。まずは下のワンページをスクロールして、紙に見える理由がどこにあるのか探してみてください。

そのページを組み立てている9個のパーツを、下のグリッドに1つずつ取り出して並べました。順番は人気順ではなく、コラージュ デザインとして1枚の紙が画面の上でたどる順序です。切って置く、位置を移す、重ねる、スタンプする、角を折る、テープで留める、ちぎる、流す、最後に押し込む、という流れになっています。同じキットの軸で艶を消して余白で信頼を稼ぐ側はsaas サイト デザイン 信頼感キットに、逆に質感をさらに重ねる側はコラージュ デザインにあります。三つのキットを並べて質感の濃度だけを選べば、LPの口調が混ざりません。紙の山は上から二番目の層を一番明るくし、下へ行くほど一段ずつ暗く決めました。九つのセクションのうち三枚はこの規則から外れないよう、影の角度まで表にして確かめています。
01 hero-paper
02 nav-paper
03 card-grid-paper
04 feature-list-paper
05 pricing-paper
06 testimonial-paper
07 cta-band-paper
08 footer-paper
09 buttons-inputs-paper
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 切り貼りのヒーロー
- 02 持ち上がる紙タブナビ
- 03 色紙の帯カードグリッド
- 04 シールのチェックリスト
- 05 角折れ料金プラン
- 06 テープ留めのレビュー
- 07 ちぎれ端のCTA帯
- 08 色紙ストリップのフッター
- 09 紙のボタン・入力欄
01切り貼りのヒーロー
切り絵 デザインとして何が見えるか
白い紙が浮き上がるあいだ、背面の色紙3枚は逆に沈みます。動く向きが逆になって初めて、重なりが層として見えます。

<main class="stage">
<section class="heropaper">
<span class="heropaper__scrap heropaper__scrap--coral" aria-hidden="true"></span>
<span class="heropaper__scrap heropaper__scrap--butter" aria-hidden="true"></span>
<span class="heropaper__scrap heropaper__scrap--leaf" aria-hidden="true"></span>
<div class="heropaper__sheet">
<span class="heropaper__badge" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M40.2 95.8a28 28 0 1 1 39.6 0a28 28 0 0 1-39.6 0m0 64.4a28 28 0 1 0 39.6 0a28 28 0 0 0-39.6 0" opacity=".2"/><path d="M157.73 113.13a8 8 0 0 1 2.09-11.13l67.66-46.3a8 8 0 0 1 9 13.21l-67.67 46.3a7.9 7.9 0 0 1-4.51 1.4a8 8 0 0 1-6.57-3.48m80.87 85.09a8 8 0 0 1-11.12 2.08L136 137.7l-42.51 29.08a36 36 0 1 1-9-13.19L121.83 128l-37.39-25.59a35.86 35.86 0 1 1 9-13.19l143 97.87a8 8 0 0 1 2.16 11.13M80 180a20 20 0 1 0-5.86 14.14A19.85 19.85 0 0 0 80 180m-5.86-89.87a20 20 0 1 0-28.28 0a19.85 19.85 0 0 0 28.28 0"/></g></svg>
</span>
<p class="heropaper__eyebrow">Cut & paste</p>
<h1 class="heropaper__title">切って貼った画面</h1>
<p class="heropaper__desc">色紙数枚でできるランディング</p>
<button class="heropaper__cta" type="button">今すぐ始める</button>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 페이퍼컷 팔레트 — 따뜻한 오프화이트 두 장 + 오려낸 색종이 다섯 색.
// 대비 실측(WCAG 상대휘도 직접 계산): ink/paper 14.34:1 · stone/paper 7.23:1 · sheet/coral 4.93:1
$paper-base: #fbf6ec;
$paper-sheet: #ffffff;
$paper-ink: #2a2320;
$paper-stone: #5c5048;
$paper-coral: #b97b71;
$paper-sky: #6a8db5;
$paper-leaf: #2b7d5a;
$paper-butter: #d6c9ab;
$paper-edge: #e7dccb;
$duration: $dur-loop; // 종이 한 장이 들렸다 내려앉는 한 주기
$easing: $ease-out; // 종이는 튀지 않는다. 되돌아올 때도 같은 속도
$color: $paper-coral; // 오려낸 색종이 조각의 대표색
@include stage($paper-base);
.heropaper {
position: relative;
width: clamp(216px, 74vw, 328px);
isolation: isolate;
}
// 뒤에 깔리는 색종이 조각 — 각자 다른 각도로 오려 붙인 것처럼
.heropaper__scrap {
position: absolute;
z-index: 0;
animation: scrapDrift $duration $easing infinite;
}
.heropaper__scrap--coral {
left: -20px;
top: 4px;
width: 76px;
height: 76px;
border-radius: 50%;
background: $paper-coral;
}
.heropaper__scrap--butter {
right: -16px;
top: -20px;
width: 92px;
height: 48px;
border-radius: $r-card;
background: $paper-butter;
transform: rotate(-8deg);
animation-delay: -.66s;
}
.heropaper__scrap--leaf {
right: 8px;
bottom: -24px;
width: 64px;
height: 40px;
border-radius: $r-pill;
background: $paper-leaf;
animation-delay: -1.33s;
}
// 맨 위 흰 종이 — 들어올려지면 그림자가 같이 자란다
.heropaper__sheet {
position: relative;
z-index: 1;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $sp-2;
padding: $sp-5;
border-radius: $r-card;
background: $paper-sheet;
box-shadow: 0 10px 18px rgba($paper-ink, .14);
animation: sheetLift $duration $easing infinite;
}
.heropaper__badge {
position: absolute;
top: -14px;
right: $sp-4;
width: 34px;
aspect-ratio: 1;
display: grid;
place-items: center;
border-radius: 50%;
background: $paper-sheet;
color: $paper-coral;
font-size: 18px;
box-shadow: 0 4px 8px rgba($paper-ink, .18);
}
.heropaper__eyebrow {
margin: 0;
color: $paper-coral;
font-weight: 700; font-size: clamp(10px, 2.6vw, 12px); line-height: 1;
font-family: inherit;
letter-spacing: .1em;
text-transform: uppercase;
}
.heropaper__title {
margin: 0;
color: $paper-ink;
font-weight: 800; font-size: clamp(18px, 4.6vw, 26px); line-height: 1.2;
font-family: inherit;
}
.heropaper__desc {
margin: 0;
color: $paper-stone;
font-weight: 500; font-size: clamp(12px, 2.8vw, 14px); line-height: 1.4;
font-family: inherit;
}
.heropaper__cta {
margin-top: $sp-1;
border: 0;
border-radius: $r-control;
background: $paper-coral;
color: $paper-sheet;
padding: $sp-2 $sp-5;
font-weight: 700; font-size: clamp(12px, 2.8vw, 14px); line-height: 1;
font-family: inherit;
box-shadow: 0 4px 0 rgba($paper-ink, .2);
}
.heropaper__cta:focus-visible {
outline: 3px solid $paper-sky;
outline-offset: 2px;
}
// 데모: 흰 종이가 올라가는 동안 뒤 색종이는 반대 위상으로 내려간다 — 겹친 층이 분리되는 게 보인다
@keyframes sheetLift {
0%, 100% { transform: translateY(0); box-shadow: 0 10px 18px rgba($paper-ink, .14); }
50% { transform: translateY(-12px); box-shadow: 0 22px 28px rgba($paper-ink, .1); }
}
@keyframes scrapDrift {
0%, 100% { transform: translateY(0) rotate(-2deg); }
50% { transform: translateY(8px) rotate(3deg); }
}
@media (max-width: 340px), (max-height: 220px) {
.heropaper { width: 200px; }
.heropaper__sheet { padding: $sp-3 $sp-4; gap: $sp-1; }
.heropaper__eyebrow { display: none; }
.heropaper__title { font-size: 16px; line-height: 1.15; }
.heropaper__desc { display: none; }
.heropaper__cta { padding: $sp-1 $sp-4; margin-top: 0; }
.heropaper__badge { width: 24px; font-size: 13px; top: -10px; right: $sp-2; }
.heropaper__scrap--coral { width: 48px; height: 48px; left: -12px; }
.heropaper__scrap--butter { width: 56px; height: 32px; right: -8px; top: -12px; }
.heropaper__scrap--leaf { width: 44px; height: 26px; bottom: -14px; }
}
@media (prefers-reduced-motion: reduce) {
.heropaper__sheet, .heropaper__scrap { animation: none !important; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf6ec;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.heropaper {
position: relative;
width: clamp(216px, 74vw, 328px);
isolation: isolate;
}
.heropaper__scrap {
position: absolute;
z-index: 0;
animation: scrapDrift 2s ease-out infinite;
}
.heropaper__scrap--coral {
left: -20px;
top: 4px;
width: 76px;
height: 76px;
border-radius: 50%;
background: #b97b71;
}
.heropaper__scrap--butter {
right: -16px;
top: -20px;
width: 92px;
height: 48px;
border-radius: 16px;
background: #d6c9ab;
transform: rotate(-8deg);
animation-delay: -0.66s;
}
.heropaper__scrap--leaf {
right: 8px;
bottom: -24px;
width: 64px;
height: 40px;
border-radius: 999px;
background: #2b7d5a;
animation-delay: -1.33s;
}
.heropaper__sheet {
position: relative;
z-index: 1;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 20px;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 10px 18px rgba(42, 35, 32, 0.14);
animation: sheetLift 2s ease-out infinite;
}
.heropaper__badge {
position: absolute;
top: -14px;
right: 16px;
width: 34px;
aspect-ratio: 1;
display: grid;
place-items: center;
border-radius: 50%;
background: #ffffff;
color: #b97b71;
font-size: 18px;
box-shadow: 0 4px 8px rgba(42, 35, 32, 0.18);
}
.heropaper__eyebrow {
margin: 0;
color: #b97b71;
font-weight: 700;
font-size: clamp(10px, 2.6vw, 12px);
line-height: 1;
font-family: inherit;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.heropaper__title {
margin: 0;
color: #2a2320;
font-weight: 800;
font-size: clamp(18px, 4.6vw, 26px);
line-height: 1.2;
font-family: inherit;
}
.heropaper__desc {
margin: 0;
color: #5c5048;
font-weight: 500;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.4;
font-family: inherit;
}
.heropaper__cta {
margin-top: 4px;
border: 0;
border-radius: 12px;
background: #b97b71;
color: #ffffff;
padding: 8px 20px;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1;
font-family: inherit;
box-shadow: 0 4px 0 rgba(42, 35, 32, 0.2);
}
.heropaper__cta:focus-visible {
outline: 3px solid #6a8db5;
outline-offset: 2px;
}
@keyframes sheetLift {
0%, 100% {
transform: translateY(0);
box-shadow: 0 10px 18px rgba(42, 35, 32, 0.14);
}
50% {
transform: translateY(-12px);
box-shadow: 0 22px 28px rgba(42, 35, 32, 0.1);
}
}
@keyframes scrapDrift {
0%, 100% {
transform: translateY(0) rotate(-2deg);
}
50% {
transform: translateY(8px) rotate(3deg);
}
}
@media (max-width: 340px), (max-height: 220px) {
.heropaper {
width: 200px;
}
.heropaper__sheet {
padding: 12px 16px;
gap: 4px;
}
.heropaper__eyebrow {
display: none;
}
.heropaper__title {
font-size: 16px;
line-height: 1.15;
}
.heropaper__desc {
display: none;
}
.heropaper__cta {
padding: 4px 16px;
margin-top: 0;
}
.heropaper__badge {
width: 24px;
font-size: 13px;
top: -10px;
right: 8px;
}
.heropaper__scrap--coral {
width: 48px;
height: 48px;
left: -12px;
}
.heropaper__scrap--butter {
width: 56px;
height: 32px;
right: -8px;
top: -12px;
}
.heropaper__scrap--leaf {
width: 44px;
height: 26px;
bottom: -14px;
}
}
@media (prefers-reduced-motion: reduce) {
.heropaper__sheet, .heropaper__scrap {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./HeroPaper.module.scss";
export type HeroPaperProps = {
eyebrow?: string;
title?: string;
desc?: string;
cta?: string;
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function HeroPaper({
eyebrow = "Cut & paste",
title = "오려 붙인 화면",
desc = "색종이 몇 장이면 되는 랜딩",
cta = "바로 시작",
duration = 2000,
easing = "ease-out",
color = "#cc3d26",
}: HeroPaperProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.heropaper} style={vars}>
<span className={`${styles.scrap} ${styles.scrapCoral}`} aria-hidden="true" />
<span className={`${styles.scrap} ${styles.scrapButter}`} aria-hidden="true" />
<span className={`${styles.scrap} ${styles.scrapLeaf}`} aria-hidden="true" />
<div className={styles.sheet}>
<span className={styles.badge} aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M40.2 95.8a28 28 0 1 1 39.6 0a28 28 0 0 1-39.6 0m0 64.4a28 28 0 1 0 39.6 0a28 28 0 0 0-39.6 0" opacity=".2"/><path d="M157.73 113.13a8 8 0 0 1 2.09-11.13l67.66-46.3a8 8 0 0 1 9 13.21l-67.67 46.3a7.9 7.9 0 0 1-4.51 1.4a8 8 0 0 1-6.57-3.48m80.87 85.09a8 8 0 0 1-11.12 2.08L136 137.7l-42.51 29.08a36 36 0 1 1-9-13.19L121.83 128l-37.39-25.59a35.86 35.86 0 1 1 9-13.19l143 97.87a8 8 0 0 1 2.16 11.13M80 180a20 20 0 1 0-5.86 14.14A19.85 19.85 0 0 0 80 180m-5.86-89.87a20 20 0 1 0-28.28 0a19.85 19.85 0 0 0 28.28 0"/></g></svg>
</span>
<p className={styles.eyebrow}>{eyebrow}</p>
<h1 className={styles.title}>{title}</h1>
<p className={styles.desc}>{desc}</p>
<button type="button" className={styles.cta}>{cta}</button>
</div>
</section>
);
}01 · 切り貼りのヒーロー — 計測 累積面積 14.784% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードのどこを見るか
影は紙が上がった分だけ広がって薄くなります。
.heropaper__sheet {
box-shadow: 0 10px 18px rgba(42, 35, 32, .14);
animation: sheetLift 2s ease-out infinite;
}
.heropaper__scrap { animation: scrapDrift 2s ease-out infinite; }
.heropaper__scrap--butter { animation-delay: -.66s; }
.heropaper__scrap--leaf { animation-delay: -1.33s; }
@keyframes sheetLift {
0%, 100% { transform: translateY(0); box-shadow: 0 10px 18px rgba(42, 35, 32, .14); }
50% { transform: translateY(-12px); box-shadow: 0 22px 28px rgba(42, 35, 32, .1); }
}
@keyframes scrapDrift {
0%, 100% { transform: translateY(0) rotate(-2deg); }
50% { transform: translateY(8px) rotate(3deg); }
}
02持ち上がる紙タブナビ
画面で起きること
下線が滑る代わりに、黄色い紙のチップがタブからタブへ跳んで移ります。

<main class="stage">
<nav class="navpaper" aria-label="デモナビゲーション">
<span class="navpaper__brand">工作室</span>
<div class="navpaper__tabs">
<span class="navpaper__chip" aria-hidden="true"></span>
<span class="navpaper__tab">ホーム</span>
<span class="navpaper__tab">素材</span>
<span class="navpaper__tab">料金</span>
</div>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
// 페이퍼컷 팔레트 (01 과 같은 값. zip 에서는 tokens.css 한 곳을 링크한다)
$paper-base: #fbf6ec;
$paper-sheet: #ffffff;
$paper-ink: #2a2320;
$paper-stone: #5c5048;
$paper-coral: #b97b71;
$paper-sky: #6a8db5;
$paper-leaf: #2b7d5a;
$paper-butter: #d6c9ab;
$paper-edge: #e7dccb;
$duration: $dur-loop; // 칩이 세 탭을 왕복하는 한 주기
$easing: $ease-spring; // 종이 조각이 자리에 앉는 느낌
$color: $paper-butter; // 들린 탭 종이의 색
$step: 76px; // 탭 하나 + 간격. 칩 이동량은 이 값의 배수뿐이다
@include stage($paper-sheet);
.navpaper {
box-sizing: border-box;
display: flex;
align-items: center;
gap: $sp-4;
width: clamp(268px, 82vw, 372px);
padding: $sp-3 $sp-4;
border-radius: $r-pill;
background: $paper-base;
box-shadow: 0 6px 0 $paper-edge;
}
.navpaper__brand {
flex: 0 0 auto;
color: $paper-ink;
font-weight: 800; font-size: 14px; line-height: 1;
font-family: inherit;
white-space: nowrap;
}
.navpaper__tabs {
position: relative;
display: flex;
gap: $sp-1;
--step: #{$step};
}
// 들린 종이 칩 — 탭 뒤에서 움직인다. 탭은 고정 폭이라 이동량과 실제 렌더 폭이 어긋나지 않는다 (BD19)
.navpaper__chip {
position: absolute;
left: 0;
top: -4px;
width: 72px;
height: calc(100% + 8px);
border-radius: $r-control;
background: $color;
box-shadow: 0 4px 0 rgba($paper-ink, .18);
animation: chipHop $duration $easing infinite;
}
.navpaper__tab {
position: relative;
z-index: 1;
flex: 0 0 72px;
text-align: center;
color: $paper-ink;
font-weight: 700; font-size: 13px; line-height: 1.6;
font-family: inherit;
white-space: nowrap;
}
// 데모: 0 → 1 → 2 → 1 → 0 으로 왕복한다. 끝에서 처음으로 순간이동하지 않는다
@keyframes chipHop {
0% { transform: translateX(0) translateY(0); }
12% { transform: translateX(0) translateY(-5px); }
25% { transform: translateX(var(--step)) translateY(0); }
37% { transform: translateX(var(--step)) translateY(-5px); }
50% { transform: translateX(calc(var(--step) * 2)) translateY(0); }
62% { transform: translateX(calc(var(--step) * 2)) translateY(-5px); }
75% { transform: translateX(var(--step)) translateY(0); }
87% { transform: translateX(var(--step)) translateY(-5px); }
100% { transform: translateX(0) translateY(0); }
}
@media (max-width: 340px), (max-height: 220px) {
.navpaper { width: 244px; gap: $sp-2; padding: $sp-2 $sp-3; }
.navpaper__brand { font-size: 12px; }
.navpaper__tabs { --step: 56px; }
.navpaper__chip { width: 52px; }
.navpaper__tab { flex: 0 0 52px; font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.navpaper__chip { animation: none !important; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.navpaper {
box-sizing: border-box;
display: flex;
align-items: center;
gap: 16px;
width: clamp(268px, 82vw, 372px);
padding: 12px 16px;
border-radius: 999px;
background: #fbf6ec;
box-shadow: 0 6px 0 #e7dccb;
}
.navpaper__brand {
flex: 0 0 auto;
color: #2a2320;
font-weight: 800;
font-size: 14px;
line-height: 1;
font-family: inherit;
white-space: nowrap;
}
.navpaper__tabs {
position: relative;
display: flex;
gap: 4px;
--step: 76px;
}
.navpaper__chip {
position: absolute;
left: 0;
top: -4px;
width: 72px;
height: calc(100% + 8px);
border-radius: 12px;
background: #d6c9ab;
box-shadow: 0 4px 0 rgba(42, 35, 32, 0.18);
animation: chipHop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.navpaper__tab {
position: relative;
z-index: 1;
flex: 0 0 72px;
text-align: center;
color: #2a2320;
font-weight: 700;
font-size: 13px;
line-height: 1.6;
font-family: inherit;
white-space: nowrap;
}
@keyframes chipHop {
0% {
transform: translateX(0) translateY(0);
}
12% {
transform: translateX(0) translateY(-5px);
}
25% {
transform: translateX(var(--step)) translateY(0);
}
37% {
transform: translateX(var(--step)) translateY(-5px);
}
50% {
transform: translateX(calc(var(--step) * 2)) translateY(0);
}
62% {
transform: translateX(calc(var(--step) * 2)) translateY(-5px);
}
75% {
transform: translateX(var(--step)) translateY(0);
}
87% {
transform: translateX(var(--step)) translateY(-5px);
}
100% {
transform: translateX(0) translateY(0);
}
}
@media (max-width: 340px), (max-height: 220px) {
.navpaper {
width: 244px;
gap: 8px;
padding: 8px 12px;
}
.navpaper__brand {
font-size: 12px;
}
.navpaper__tabs {
--step: 56px;
}
.navpaper__chip {
width: 52px;
}
.navpaper__tab {
flex: 0 0 52px;
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.navpaper__chip {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./NavPaper.module.scss";
export type NavPaperProps = {
brand?: string;
/** 탭 이름 3개. 칩은 이 개수 안에서만 왕복한다 */
tabs?: string[];
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
/** 탭 하나 + 간격. 칩 이동량은 이 값의 배수뿐이다 */
step?: number;
};
type Vars = CSSProperties & {
"--duration"?: string; "--easing"?: string; "--color"?: string; "--step"?: string;
};
export default function NavPaper({
brand = "공작실",
tabs = ["홈", "재료", "요금"],
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#f6c453",
step = 76,
}: NavPaperProps) {
const vars: Vars = {
"--duration": `${duration}ms`, "--easing": easing, "--color": color, "--step": `${step}px`,
};
return (
<nav className={styles.navpaper} style={vars} aria-label="데모 내비게이션">
<span className={styles.brand}>{brand}</span>
<div className={styles.tabs}>
<span className={styles.chip} aria-hidden="true" />
{tabs.map((t) => <span key={t} className={styles.tab}>{t}</span>)}
</div>
</nav>
);
}02 · 持ち上がる紙タブナビ — 計測 累積面積 5.53% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードの読み方
移動のたびに5px持ち上がるので、滑っているのではなく、つまんで置き直しているように見えます。
.navpaper__tabs { --step: 76px; } /* タブ幅72 + 間隔4 */
.navpaper__tab { flex: 0 0 72px; } /* 固定幅なので移動量と実際の幅がずれない */
.navpaper__chip { animation: chipHop 2s cubic-bezier(.2, .8, .2, 1) infinite; }
@keyframes chipHop {
0% { transform: translateX(0) translateY(0); }
12% { transform: translateX(0) translateY(-5px); }
25% { transform: translateX(var(--step)) translateY(0); }
50% { transform: translateX(calc(var(--step) * 2)) translateY(0); }
75% { transform: translateX(var(--step)) translateY(0); }
100% { transform: translateX(0) translateY(0); }
}
03色紙の帯カードグリッド
どんな動きか
白いカード3枚の上端に、それぞれ違う色紙の帯を載せました。カード本体はすべて白のままで、色が入るのは帯とアイコンだけです。

<main class="stage">
<section class="cardpaper">
<article class="cardpaper__card cardpaper__card--coral">
<span class="cardpaper__ic" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m224 80l-96 56l-96-56l96-56Z" opacity=".2"/><path d="M230.91 172a8 8 0 0 1-2.91 10.91l-96 56a8 8 0 0 1-8.06 0l-96-56A8 8 0 0 1 36 169.09l92 53.65l92-53.65a8 8 0 0 1 10.91 2.91M220 121.09l-92 53.65l-92-53.65a8 8 0 0 0-8 13.82l96 56a8 8 0 0 0 8.06 0l96-56a8 8 0 1 0-8.06-13.82M24 80a8 8 0 0 1 4-6.91l96-56a8 8 0 0 1 8.06 0l96 56a8 8 0 0 1 0 13.82l-96 56a8 8 0 0 1-8.06 0l-96-56A8 8 0 0 1 24 80m23.88 0L128 126.74L208.12 80L128 33.26Z"/></g></svg>
</span>
<h2 class="cardpaper__t">重ねる</h2>
<p class="cardpaper__d">奥行きは層でつくる</p>
</article>
<article class="cardpaper__card cardpaper__card--sky">
<span class="cardpaper__ic" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M224 32c0 32.81-31.64 67.43-58.64 91.05A84.4 84.4 0 0 0 133 90.64c23.57-27 58.19-58.64 91-58.64" opacity=".2"/><path d="M232 32a8 8 0 0 0-8-8c-44.08 0-89.31 49.71-114.43 82.63A60 60 0 0 0 32 164c0 30.88-19.54 44.73-20.47 45.37A8 8 0 0 0 16 224h76a60 60 0 0 0 57.37-77.57C182.3 121.31 232 76.08 232 32M92 208H34.63C41.38 198.41 48 183.92 48 164a44 44 0 1 1 44 44m32.42-94.45q5.14-6.66 10.09-12.55A76.2 76.2 0 0 1 155 121.49q-5.9 4.94-12.55 10.09a60.5 60.5 0 0 0-18.03-18.03m42.7-2.68a92.6 92.6 0 0 0-22-22c31.78-34.53 55.75-45 69.9-47.91c-2.85 14.16-13.37 38.13-47.9 69.91"/></g></svg>
</span>
<h2 class="cardpaper__t">色を塗る</h2>
<p class="cardpaper__d">色は3色まで</p>
</article>
<article class="cardpaper__card cardpaper__card--leaf">
<span class="cardpaper__ic" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m194.82 151.43l-55.09 20.3l-20.3 55.09a7.92 7.92 0 0 1-14.86 0l-20.3-55.09l-55.09-20.3a7.92 7.92 0 0 1 0-14.86l55.09-20.3l20.3-55.09a7.92 7.92 0 0 1 14.86 0l20.3 55.09l55.09 20.3a7.92 7.92 0 0 1 0 14.86" opacity=".2"/><path d="M197.58 129.06L146 110l-19-51.62a15.92 15.92 0 0 0-29.88 0L78 110l-51.62 19a15.92 15.92 0 0 0 0 29.88L78 178l19 51.62a15.92 15.92 0 0 0 29.88 0L146 178l51.62-19a15.92 15.92 0 0 0 0-29.88ZM137 164.22a8 8 0 0 0-4.74 4.74L112 223.85L91.78 169a8 8 0 0 0-4.78-4.78L32.15 144L87 123.78a8 8 0 0 0 4.78-4.78L112 64.15L132.22 119a8 8 0 0 0 4.74 4.74L191.85 144ZM144 40a8 8 0 0 1 8-8h16V16a8 8 0 0 1 16 0v16h16a8 8 0 0 1 0 16h-16v16a8 8 0 0 1-16 0V48h-16a8 8 0 0 1-8-8m104 48a8 8 0 0 1-8 8h-8v8a8 8 0 0 1-16 0v-8h-8a8 8 0 0 1 0-16h8v-8a8 8 0 0 1 16 0v8h8a8 8 0 0 1 8 8"/></g></svg>
</span>
<h2 class="cardpaper__t">貼る</h2>
<p class="cardpaper__d">影で留める</p>
</article>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 페이퍼컷 팔레트
$paper-base: #fbf6ec;
$paper-sheet: #ffffff;
$paper-ink: #2a2320;
$paper-stone: #5c5048;
$paper-coral: #b97b71;
$paper-sky: #6a8db5;
$paper-leaf: #2b7d5a;
$paper-butter: #d6c9ab;
$paper-edge: #e7dccb;
$duration: $dur-loop; // 카드 한 장이 들렸다 앉는 주기. 셋은 같은 주기를 시차로 나눠 쓴다
$easing: $ease-spring; // 위로 뜰 때만 살짝 탄력
$color: $paper-sheet; // 카드 본체는 흰 종이, 색은 머리띠에만
@include stage($paper-butter);
.cardpaper {
display: flex;
gap: $sp-3;
align-items: stretch; // 세 장의 높이를 맞춘다. flex-end 로 두면 글 길이만큼 아랫단이 어긋난다
}
.cardpaper__card {
box-sizing: border-box;
position: relative;
width: 120px;
padding: $sp-6 $sp-3 $sp-4;
border-radius: $r-card;
background: $color;
box-shadow: 0 6px 0 rgba($paper-ink, .16);
animation: cardPeel $duration $easing infinite;
}
// 카드 위 오려 붙인 머리띠 — 카드마다 다른 색종이
.cardpaper__card::before {
content: "";
position: absolute;
left: $sp-3;
right: $sp-3;
top: -8px;
height: $sp-4;
border-radius: $r-xs;
}
.cardpaper__card--coral::before { background: $paper-coral; }
.cardpaper__card--sky::before { background: $paper-sky; }
.cardpaper__card--leaf::before { background: $paper-leaf; }
.cardpaper__card--sky { animation-delay: -.22s; }
.cardpaper__card--leaf { animation-delay: -.44s; }
.cardpaper__ic {
display: block;
font-size: 26px;
line-height: 1;
margin-bottom: $sp-2;
}
.cardpaper__card--coral .cardpaper__ic { color: $paper-coral; }
.cardpaper__card--sky .cardpaper__ic { color: $paper-sky; }
.cardpaper__card--leaf .cardpaper__ic { color: $paper-leaf; }
.cardpaper__t {
margin: 0 0 $sp-1;
color: $paper-ink;
font-weight: 800; font-size: 15px; line-height: 1.2;
font-family: inherit;
}
.cardpaper__d {
margin: 0;
color: $paper-stone;
font-weight: 500; font-size: 12px; line-height: 1.35;
font-family: inherit;
}
// 데모: 세 장이 0.22초씩 어긋나 차례로 들린다 — 겹쳐 붙인 종이가 한 장씩 떨어지는 것처럼
@keyframes cardPeel {
0%, 100% { transform: translateY(0) rotate(0deg); box-shadow: 0 6px 0 rgba($paper-ink, .16); }
45% { transform: translateY(-14px) rotate(-2deg); box-shadow: 0 18px 8px rgba($paper-ink, .12); }
}
@media (max-width: 340px), (max-height: 220px) {
.cardpaper { gap: $sp-2; }
.cardpaper__card { width: 84px; padding: $sp-5 $sp-2 $sp-3; }
.cardpaper__ic { font-size: 20px; margin-bottom: $sp-1; }
.cardpaper__t { font-size: 13px; }
.cardpaper__d { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.cardpaper__card { animation: none !important; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #d6c9ab;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cardpaper {
display: flex;
gap: 12px;
align-items: stretch;
}
.cardpaper__card {
box-sizing: border-box;
position: relative;
width: 120px;
padding: 24px 12px 16px;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 6px 0 rgba(42, 35, 32, 0.16);
animation: cardPeel 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.cardpaper__card::before {
content: "";
position: absolute;
left: 12px;
right: 12px;
top: -8px;
height: 16px;
border-radius: 4px;
}
.cardpaper__card--coral::before {
background: #b97b71;
}
.cardpaper__card--sky::before {
background: #6a8db5;
}
.cardpaper__card--leaf::before {
background: #2b7d5a;
}
.cardpaper__card--sky {
animation-delay: -0.22s;
}
.cardpaper__card--leaf {
animation-delay: -0.44s;
}
.cardpaper__ic {
display: block;
font-size: 26px;
line-height: 1;
margin-bottom: 8px;
}
.cardpaper__card--coral .cardpaper__ic {
color: #b97b71;
}
.cardpaper__card--sky .cardpaper__ic {
color: #6a8db5;
}
.cardpaper__card--leaf .cardpaper__ic {
color: #2b7d5a;
}
.cardpaper__t {
margin: 0 0 4px;
color: #2a2320;
font-weight: 800;
font-size: 15px;
line-height: 1.2;
font-family: inherit;
}
.cardpaper__d {
margin: 0;
color: #5c5048;
font-weight: 500;
font-size: 12px;
line-height: 1.35;
font-family: inherit;
}
@keyframes cardPeel {
0%, 100% {
transform: translateY(0) rotate(0deg);
box-shadow: 0 6px 0 rgba(42, 35, 32, 0.16);
}
45% {
transform: translateY(-14px) rotate(-2deg);
box-shadow: 0 18px 8px rgba(42, 35, 32, 0.12);
}
}
@media (max-width: 340px), (max-height: 220px) {
.cardpaper {
gap: 8px;
}
.cardpaper__card {
width: 84px;
padding: 20px 8px 12px;
}
.cardpaper__ic {
font-size: 20px;
margin-bottom: 4px;
}
.cardpaper__t {
font-size: 13px;
}
.cardpaper__d {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.cardpaper__card {
animation: none !important;
transform: none;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./CardGridPaper.module.scss";
export type PaperCard = { title: string; desc: string; band: "coral" | "sky" | "leaf"; icon: "stack" | "brush" | "sparkle" };
export type CardGridPaperProps = {
cards?: PaperCard[];
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
/** 카드 한 장이 늦게 들리는 간격(ms) */
stagger?: number;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--delay"?: string };
const ICON: Record<PaperCard["icon"], ReactNode> = {
stack: <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m224 80l-96 56l-96-56l96-56Z" opacity=".2"/><path d="M230.91 172a8 8 0 0 1-2.91 10.91l-96 56a8 8 0 0 1-8.06 0l-96-56A8 8 0 0 1 36 169.09l92 53.65l92-53.65a8 8 0 0 1 10.91 2.91M220 121.09l-92 53.65l-92-53.65a8 8 0 0 0-8 13.82l96 56a8 8 0 0 0 8.06 0l96-56a8 8 0 1 0-8.06-13.82M24 80a8 8 0 0 1 4-6.91l96-56a8 8 0 0 1 8.06 0l96 56a8 8 0 0 1 0 13.82l-96 56a8 8 0 0 1-8.06 0l-96-56A8 8 0 0 1 24 80m23.88 0L128 126.74L208.12 80L128 33.26Z"/></g></svg>,
brush: <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M224 32c0 32.81-31.64 67.43-58.64 91.05A84.4 84.4 0 0 0 133 90.64c23.57-27 58.19-58.64 91-58.64" opacity=".2"/><path d="M232 32a8 8 0 0 0-8-8c-44.08 0-89.31 49.71-114.43 82.63A60 60 0 0 0 32 164c0 30.88-19.54 44.73-20.47 45.37A8 8 0 0 0 16 224h76a60 60 0 0 0 57.37-77.57C182.3 121.31 232 76.08 232 32M92 208H34.63C41.38 198.41 48 183.92 48 164a44 44 0 1 1 44 44m32.42-94.45q5.14-6.66 10.09-12.55A76.2 76.2 0 0 1 155 121.49q-5.9 4.94-12.55 10.09a60.5 60.5 0 0 0-18.03-18.03m42.7-2.68a92.6 92.6 0 0 0-22-22c31.78-34.53 55.75-45 69.9-47.91c-2.85 14.16-13.37 38.13-47.9 69.91"/></g></svg>,
sparkle: <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m194.82 151.43l-55.09 20.3l-20.3 55.09a7.92 7.92 0 0 1-14.86 0l-20.3-55.09l-55.09-20.3a7.92 7.92 0 0 1 0-14.86l55.09-20.3l20.3-55.09a7.92 7.92 0 0 1 14.86 0l20.3 55.09l55.09 20.3a7.92 7.92 0 0 1 0 14.86" opacity=".2"/><path d="M197.58 129.06L146 110l-19-51.62a15.92 15.92 0 0 0-29.88 0L78 110l-51.62 19a15.92 15.92 0 0 0 0 29.88L78 178l19 51.62a15.92 15.92 0 0 0 29.88 0L146 178l51.62-19a15.92 15.92 0 0 0 0-29.88ZM137 164.22a8 8 0 0 0-4.74 4.74L112 223.85L91.78 169a8 8 0 0 0-4.78-4.78L32.15 144L87 123.78a8 8 0 0 0 4.78-4.78L112 64.15L132.22 119a8 8 0 0 0 4.74 4.74L191.85 144ZM144 40a8 8 0 0 1 8-8h16V16a8 8 0 0 1 16 0v16h16a8 8 0 0 1 0 16h-16v16a8 8 0 0 1-16 0V48h-16a8 8 0 0 1-8-8m104 48a8 8 0 0 1-8 8h-8v8a8 8 0 0 1-16 0v-8h-8a8 8 0 0 1 0-16h8v-8a8 8 0 0 1 16 0v8h8a8 8 0 0 1 8 8"/></g></svg>,
};
const BAND: Record<PaperCard["band"], string> = { coral: styles.bandCoral, sky: styles.bandSky, leaf: styles.bandLeaf };
export default function CardGridPaper({
cards = [
{ title: "겹치기", desc: "깊이는 층으로", band: "coral", icon: "stack" },
{ title: "칠하기", desc: "색은 세 가지만", band: "sky", icon: "brush" },
{ title: "붙이기", desc: "그림자로 고정", band: "leaf", icon: "sparkle" },
],
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
stagger = 220,
}: CardGridPaperProps) {
return (
<section className={styles.cardpaper}>
{cards.map((c, i) => {
const vars: Vars = {
"--duration": `${duration}ms`, "--easing": easing, "--delay": `${-stagger * i}ms`,
};
return (
<article key={c.title} className={`${styles.card} ${BAND[c.band]}`} style={vars}>
<span className={styles.ic} aria-hidden="true">{ICON[c.icon]}</span>
<h2 className={styles.t}>{c.title}</h2>
<p className={styles.d}>{c.desc}</p>
</article>
);
})}
</section>
);
}03 · 色紙の帯カードグリッド — 計測 累積面積 17.899% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
どこを変えるか
3枚は0.22秒ずつずれて順番に持ち上がります。
.cardpaper { display: flex; gap: 12px; align-items: stretch; }
.cardpaper__card {
box-shadow: 0 6px 0 rgba(42, 35, 32, .16); /* ぼかし0 = 紙の厚み */
animation: cardPeel 2s cubic-bezier(.2, .8, .2, 1) infinite;
}
.cardpaper__card::before { /* 上端に載せた色紙の帯 */
content: ""; position: absolute; left: 12px; right: 12px; top: -8px;
height: 16px; border-radius: 4px;
}
.cardpaper__card--sky { animation-delay: -.22s; }
.cardpaper__card--leaf { animation-delay: -.44s; }
@keyframes cardPeel {
0%, 100% { transform: translateY(0); box-shadow: 0 6px 0 rgba(42, 35, 32, .16); }
45% { transform: translateY(-14px) rotate(-2deg); box-shadow: 0 18px 8px rgba(42, 35, 32, .12); }
}
04シールのチェックリスト
何が見えるか
丸いシール4枚が順番に貼り付きます。0.45倍まで潰れ、1.18倍に跳ね返り、元に戻るという3段階が、判子を押したような手応えを作ります。

<main class="stage">
<ul class="stickerlist">
<li class="stickerlist__row">
<span class="stickerlist__dot" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"/></g></svg>
</span>
<span class="stickerlist__label">色は最大3色まで</span>
</li>
<li class="stickerlist__row">
<span class="stickerlist__dot" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"/></g></svg>
</span>
<span class="stickerlist__label">影は一方向だけ</span>
</li>
<li class="stickerlist__row">
<span class="stickerlist__dot" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"/></g></svg>
</span>
<span class="stickerlist__label">角は同じ半径で</span>
</li>
<li class="stickerlist__row">
<span class="stickerlist__dot" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"/></g></svg>
</span>
<span class="stickerlist__label">文字は一色のインクで</span>
</li>
</ul>
</main>@use "../../../../site/static/demo-tokens" as *;
// 페이퍼컷 팔레트
$paper-base: #fbf6ec;
$paper-sheet: #ffffff;
$paper-ink: #2a2320;
$paper-stone: #5c5048;
$paper-coral: #b97b71;
$paper-sky: #6a8db5;
$paper-leaf: #2b7d5a;
$paper-butter: #d6c9ab;
$paper-edge: #e7dccb;
$duration: $dur-loop; // 스티커 네 장이 차례로 붙는 한 주기
$easing: $ease-pop; // 붙는 순간만 살짝 튄다
$color: $paper-leaf; // 스티커 바탕색
@include stage($paper-base);
.stickerlist {
margin: 0;
padding: $sp-5;
list-style: none;
width: clamp(232px, 76vw, 320px);
box-sizing: border-box;
border-radius: $r-card;
background: $paper-sheet;
box-shadow: 0 6px 0 $paper-edge;
}
.stickerlist__row {
display: flex;
align-items: center;
gap: $sp-3;
}
.stickerlist__row + .stickerlist__row { margin-top: $sp-3; }
// 동그란 스티커 — 네 장이 0.25초씩 어긋나 찍힌다
.stickerlist__dot {
flex: 0 0 auto;
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 50%;
background: $color;
color: $paper-sheet;
font-size: 20px;
box-shadow: 0 3px 0 rgba($paper-ink, .2);
animation: stickerStamp $duration $easing infinite;
}
.stickerlist__row:nth-child(2) .stickerlist__dot { animation-delay: -1.75s; background: $paper-coral; }
.stickerlist__row:nth-child(3) .stickerlist__dot { animation-delay: -1.5s; background: $paper-sky; }
.stickerlist__row:nth-child(4) .stickerlist__dot { animation-delay: -1.25s; background: $paper-butter; color: $paper-ink; }
.stickerlist__label {
color: $paper-ink;
font-weight: 600; font-size: 13px; line-height: 1.4;
font-family: inherit;
}
// 데모: 스티커가 작게 눌렸다가 제자리로 — 네 장이 순서대로 찍힌다
@keyframes stickerStamp {
0%, 60%, 100% { transform: scale(1) rotate(0deg); }
10% { transform: scale(.45) rotate(-12deg); }
22% { transform: scale(1.18) rotate(6deg); }
34% { transform: scale(1) rotate(0deg); }
}
@media (max-width: 340px), (max-height: 220px) {
.stickerlist { padding: $sp-3; width: 248px; }
.stickerlist__row + .stickerlist__row { margin-top: $sp-2; }
.stickerlist__dot { width: 22px; height: 22px; font-size: 15px; }
.stickerlist__label { font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.stickerlist__dot { animation: none !important; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf6ec;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stickerlist {
margin: 0;
padding: 20px;
list-style: none;
width: clamp(232px, 76vw, 320px);
box-sizing: border-box;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 6px 0 #e7dccb;
}
.stickerlist__row {
display: flex;
align-items: center;
gap: 12px;
}
.stickerlist__row + .stickerlist__row {
margin-top: 12px;
}
.stickerlist__dot {
flex: 0 0 auto;
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 50%;
background: #2b7d5a;
color: #ffffff;
font-size: 20px;
box-shadow: 0 3px 0 rgba(42, 35, 32, 0.2);
animation: stickerStamp 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.stickerlist__row:nth-child(2) .stickerlist__dot {
animation-delay: -1.75s;
background: #b97b71;
}
.stickerlist__row:nth-child(3) .stickerlist__dot {
animation-delay: -1.5s;
background: #6a8db5;
}
.stickerlist__row:nth-child(4) .stickerlist__dot {
animation-delay: -1.25s;
background: #d6c9ab;
color: #2a2320;
}
.stickerlist__label {
color: #2a2320;
font-weight: 600;
font-size: 13px;
line-height: 1.4;
font-family: inherit;
}
@keyframes stickerStamp {
0%, 60%, 100% {
transform: scale(1) rotate(0deg);
}
10% {
transform: scale(0.45) rotate(-12deg);
}
22% {
transform: scale(1.18) rotate(6deg);
}
34% {
transform: scale(1) rotate(0deg);
}
}
@media (max-width: 340px), (max-height: 220px) {
.stickerlist {
padding: 12px;
width: 248px;
}
.stickerlist__row + .stickerlist__row {
margin-top: 8px;
}
.stickerlist__dot {
width: 22px;
height: 22px;
font-size: 15px;
}
.stickerlist__label {
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.stickerlist__dot {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./StickerList.module.scss";
export type StickerListProps = {
items?: string[];
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
/** 스티커 색. 줄 순서대로 돈다 */
colors?: string[];
};
type Vars = CSSProperties & {
"--duration"?: string; "--easing"?: string; "--dot"?: string; "--delay"?: string; "--mark"?: string;
};
const CHECK = (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"/></g></svg>
);
export default function StickerList({
items = ["색은 세 가지까지만", "그림자는 한 방향", "모서리는 같은 반경", "글자는 잉크색 하나"],
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
colors = ["#2b7d5a", "#cc3d26", "#2f6fb8", "#f6c453"],
}: StickerListProps) {
return (
<ul className={styles.stickerlist}>
{items.map((label, i) => {
const dot = colors[i % colors.length];
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--dot": dot,
"--mark": dot === "#f6c453" ? "#2a2320" : "#fff",
"--delay": `${-duration + (duration / items.length) * i}ms`,
};
return (
<li key={label} className={styles.row}>
<span className={styles.dot} style={vars} aria-hidden="true">{CHECK}</span>
<span className={styles.label}>{label}</span>
</li>
);
})}
</ul>
);
}04 · シールのチェックリスト — 計測 累積面積 3.438% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードのどこを見るか
シールは4枚で色も4色なので、1枚が1色ずつ受け持ちます。
.stickerlist__dot {
box-shadow: 0 3px 0 rgba(42, 35, 32, .2);
animation: stickerStamp 2s cubic-bezier(.2, 1.4, .4, 1) infinite;
}
.stickerlist__row:nth-child(2) .stickerlist__dot { animation-delay: -1.75s; }
.stickerlist__row:nth-child(3) .stickerlist__dot { animation-delay: -1.5s; }
.stickerlist__row:nth-child(4) .stickerlist__dot { animation-delay: -1.25s; }
@keyframes stickerStamp {
0%, 60%, 100% { transform: scale(1) rotate(0deg); }
10% { transform: scale(.45) rotate(-12deg); }
22% { transform: scale(1.18) rotate(6deg); }
34% { transform: scale(1) rotate(0deg); }
}
05角折れ料金プラン
画面で起きること
おすすめプランの右上の角が、紙の裏面の色を見せながら折れて、また開きます。

<main class="stage">
<section class="dogear">
<article class="dogear__plan">
<p class="dogear__name">1枚</p>
<p class="dogear__price">₩0</p>
<p class="dogear__note">パーツ1個</p>
</article>
<article class="dogear__plan dogear__plan--pick">
<span class="dogear__fold" aria-hidden="true"></span>
<span class="dogear__star" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m229.06 108.79l-48.7 42l14.88 62.79a8.4 8.4 0 0 1-12.52 9.17L128 189.09l-54.72 33.65a8.4 8.4 0 0 1-12.52-9.17l14.88-62.79l-48.7-42A8.46 8.46 0 0 1 31.73 94l63.91-5.2l24.62-59.6a8.36 8.36 0 0 1 15.48 0l24.62 59.6l63.91 5.2a8.46 8.46 0 0 1 4.79 14.79" opacity=".2"/><path d="M239.18 97.26A16.38 16.38 0 0 0 224.92 86l-59-4.76l-22.78-55.09a16.36 16.36 0 0 0-30.27 0L90.11 81.23L31.08 86a16.46 16.46 0 0 0-9.37 28.86l45 38.83L53 211.75a16.38 16.38 0 0 0 24.5 17.82l50.5-31.08l50.53 31.08A16.4 16.4 0 0 0 203 211.75l-13.76-58.07l45-38.83a16.43 16.43 0 0 0 4.94-17.59m-15.34 5.47l-48.7 42a8 8 0 0 0-2.56 7.91l14.88 62.8a.37.37 0 0 1-.17.48c-.18.14-.23.11-.38 0l-54.72-33.65a8 8 0 0 0-8.38 0l-54.72 33.67c-.15.09-.19.12-.38 0a.37.37 0 0 1-.17-.48l14.88-62.8a8 8 0 0 0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16a8 8 0 0 0 6.72-4.94l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153 91.86a8 8 0 0 0 6.75 4.92l63.92 5.16c.15 0 .24 0 .33.29s0 .4-.16.5"/></g></svg>
</span>
<p class="dogear__name">ひと束</p>
<p class="dogear__price">₩9<span>,900</span></p>
<p class="dogear__note">パーツ9個セット</p>
</article>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 페이퍼컷 팔레트
$paper-base: #fbf6ec;
$paper-sheet: #ffffff;
$paper-ink: #2a2320;
$paper-stone: #5c5048;
$paper-coral: #b97b71;
$paper-sky: #6a8db5;
$paper-leaf: #2b7d5a;
$paper-butter: #d6c9ab;
$paper-edge: #e7dccb;
$paper-blush: #f6b3ab;
$duration: $dur-loop; // 모서리가 접혔다 펴지는 한 주기
$easing: $ease-out; // 종이가 스스로 펴지는 속도
$color: $paper-butter; // 접힌 모서리 안쪽(뒷면) 색
@include stage($paper-blush);
.dogear {
display: flex;
gap: $sp-3;
align-items: stretch;
}
.dogear__plan {
box-sizing: border-box;
position: relative;
width: 124px;
padding: $sp-4;
border-radius: $r-card;
background: $paper-sheet;
box-shadow: 0 6px 0 rgba($paper-ink, .16);
overflow: hidden;
}
.dogear__plan--pick {
background: $paper-sheet;
outline: 3px solid $paper-coral;
outline-offset: -3px;
}
// 오른쪽 위 접힌 모서리 — 종이 뒷면이 드러나는 삼각형
.dogear__fold {
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 40px;
background: $color;
clip-path: polygon(100% 0, 0 0, 100% 100%);
transform-origin: 100% 0; // 규칙8: dog-ear 코너 경첩
animation: cornerFold $duration $easing infinite;
}
.dogear__star {
position: absolute;
top: $sp-3;
left: $sp-3;
color: $paper-coral;
font-size: 16px;
line-height: 1;
}
.dogear__name {
margin: $sp-5 0 $sp-1;
color: $paper-stone;
font-weight: 700; font-size: 11px; line-height: 1;
font-family: inherit;
letter-spacing: .08em;
text-transform: uppercase;
}
.dogear__plan:not(.dogear__plan--pick) .dogear__name { margin-top: 0; }
.dogear__price {
margin: 0 0 $sp-1;
color: $paper-ink;
font-weight: 800; font-size: 24px; line-height: 1.1;
font-family: inherit;
}
.dogear__price span { font-size: 15px; }
.dogear__note {
margin: 0;
color: $paper-stone;
font-weight: 500; font-size: 11px; line-height: 1.35;
font-family: inherit;
}
// 데모: 모서리가 크게 접혔다가 거의 다 펴진다. 삼각형 크기가 변해 눈에 띈다
@keyframes cornerFold {
0%, 100% { transform: scale(1); }
50% { transform: scale(.28); }
}
@media (max-width: 340px), (max-height: 220px) {
.dogear { gap: $sp-2; }
.dogear__plan { width: 96px; padding: $sp-3; }
.dogear__fold { width: 28px; height: 28px; }
.dogear__star { font-size: 13px; top: $sp-2; left: $sp-2; }
.dogear__name { margin-top: $sp-4; font-size: 9px; }
.dogear__price { font-size: 18px; }
.dogear__price span { font-size: 12px; }
.dogear__note { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.dogear__fold { animation: none !important; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #f6b3ab;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.dogear {
display: flex;
gap: 12px;
align-items: stretch;
}
.dogear__plan {
box-sizing: border-box;
position: relative;
width: 124px;
padding: 16px;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 6px 0 rgba(42, 35, 32, 0.16);
overflow: hidden;
}
.dogear__plan--pick {
background: #ffffff;
outline: 3px solid #b97b71;
outline-offset: -3px;
}
.dogear__fold {
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 40px;
background: #d6c9ab;
clip-path: polygon(100% 0, 0 0, 100% 100%);
transform-origin: 100% 0;
animation: cornerFold 2s ease-out infinite;
}
.dogear__star {
position: absolute;
top: 12px;
left: 12px;
color: #b97b71;
font-size: 16px;
line-height: 1;
}
.dogear__name {
margin: 20px 0 4px;
color: #5c5048;
font-weight: 700;
font-size: 11px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.dogear__plan:not(.dogear__plan--pick) .dogear__name {
margin-top: 0;
}
.dogear__price {
margin: 0 0 4px;
color: #2a2320;
font-weight: 800;
font-size: 24px;
line-height: 1.1;
font-family: inherit;
}
.dogear__price span {
font-size: 15px;
}
.dogear__note {
margin: 0;
color: #5c5048;
font-weight: 500;
font-size: 11px;
line-height: 1.35;
font-family: inherit;
}
@keyframes cornerFold {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(0.28);
}
}
@media (max-width: 340px), (max-height: 220px) {
.dogear {
gap: 8px;
}
.dogear__plan {
width: 96px;
padding: 12px;
}
.dogear__fold {
width: 28px;
height: 28px;
}
.dogear__star {
font-size: 13px;
top: 8px;
left: 8px;
}
.dogear__name {
margin-top: 16px;
font-size: 9px;
}
.dogear__price {
font-size: 18px;
}
.dogear__price span {
font-size: 12px;
}
.dogear__note {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.dogear__fold {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./DogEarPricing.module.scss";
export type PaperPlan = { name: string; price: string; sub?: string; note: string; pick?: boolean };
export type DogEarPricingProps = {
plans?: PaperPlan[];
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
/** 접힌 모서리(종이 뒷면) 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const STAR = (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m229.06 108.79l-48.7 42l14.88 62.79a8.4 8.4 0 0 1-12.52 9.17L128 189.09l-54.72 33.65a8.4 8.4 0 0 1-12.52-9.17l14.88-62.79l-48.7-42A8.46 8.46 0 0 1 31.73 94l63.91-5.2l24.62-59.6a8.36 8.36 0 0 1 15.48 0l24.62 59.6l63.91 5.2a8.46 8.46 0 0 1 4.79 14.79" opacity=".2"/><path d="M239.18 97.26A16.38 16.38 0 0 0 224.92 86l-59-4.76l-22.78-55.09a16.36 16.36 0 0 0-30.27 0L90.11 81.23L31.08 86a16.46 16.46 0 0 0-9.37 28.86l45 38.83L53 211.75a16.38 16.38 0 0 0 24.5 17.82l50.5-31.08l50.53 31.08A16.4 16.4 0 0 0 203 211.75l-13.76-58.07l45-38.83a16.43 16.43 0 0 0 4.94-17.59m-15.34 5.47l-48.7 42a8 8 0 0 0-2.56 7.91l14.88 62.8a.37.37 0 0 1-.17.48c-.18.14-.23.11-.38 0l-54.72-33.65a8 8 0 0 0-8.38 0l-54.72 33.67c-.15.09-.19.12-.38 0a.37.37 0 0 1-.17-.48l14.88-62.8a8 8 0 0 0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16a8 8 0 0 0 6.72-4.94l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153 91.86a8 8 0 0 0 6.75 4.92l63.92 5.16c.15 0 .24 0 .33.29s0 .4-.16.5"/></g></svg>
);
export default function DogEarPricing({
plans = [
{ name: "낱장", price: "₩0", note: "부품 1개" },
{ name: "한 묶음", price: "₩9", sub: ",900", note: "부품 9개 묶음", pick: true },
],
duration = 2000,
easing = "ease-out",
color = "#f6c453",
}: DogEarPricingProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.dogear} style={vars}>
{plans.map((p) => (
<article key={p.name} className={`${styles.plan} ${p.pick ? styles.pick : ""}`}>
{p.pick && <span className={styles.fold} aria-hidden="true" />}
{p.pick && <span className={styles.star} aria-hidden="true">{STAR}</span>}
<p className={styles.name}>{p.name}</p>
<p className={styles.price}>{p.price}{p.sub && <span>{p.sub}</span>}</p>
<p className={styles.note}>{p.note}</p>
</article>
))}
</section>
);
}05 · 角折れ料金プラン — 計測 累積面積 0.525% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードの読み方
三角形はclip-pathで一度切り抜いておき、折れる動きは角の頂点に軸を置いたscaleひとつで済ませます。
.dogear__plan { position: relative; overflow: hidden; }
.dogear__fold {
position: absolute; top: 0; right: 0; width: 40px; height: 40px;
background: #f6c453; /* 紙の裏面の色 */
clip-path: polygon(100% 0, 0 0, 100% 100%);
transform-origin: 100% 0; /* 角の頂点を軸に折る */
animation: cornerFold 2s ease-out infinite;
}
@keyframes cornerFold {
0%, 100% { transform: scale(1); }
50% { transform: scale(.28); }
}
06テープ留めのレビュー
どんな動きか
メモの上辺の両端をマスキングテープで押さえています。

<main class="stage">
<figure class="tapenote">
<span class="tapenote__tape tapenote__tape--l" aria-hidden="true"></span>
<span class="tapenote__tape tapenote__tape--r" aria-hidden="true"></span>
<span class="tapenote__q" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M108 72v72H40a8 8 0 0 1-8-8V72a8 8 0 0 1 8-8h60a8 8 0 0 1 8 8m108-8h-60a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h68V72a8 8 0 0 0-8-8" opacity=".2"/><path d="M100 56H40a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80H40V72h60Zm116-80h-60a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80h-60V72h60Z"/></g></svg>
</span>
<blockquote class="tapenote__body">色紙3枚でファーストビューが完成しました。</blockquote>
<figcaption class="tapenote__by">
<span class="tapenote__ava" aria-hidden="true">ㅂ</span>
<span class="tapenote__who">バイブコーダー・個人開発</span>
</figcaption>
</figure>
</main>@use "../../../../site/static/demo-tokens" as *;
// 페이퍼컷 팔레트
$paper-base: #fbf6ec;
$paper-sheet: #ffffff;
$paper-ink: #2a2320;
$paper-stone: #5c5048;
$paper-coral: #b97b71;
$paper-sky: #6a8db5;
$paper-leaf: #2b7d5a;
$paper-butter: #d6c9ab;
$paper-edge: #e7dccb;
$duration: $dur-loop; // 메모가 좌우로 한 번 흔들리는 주기
$easing: $ease-spring; // 테이프에 매달린 종이의 감쇠
$color: $paper-butter; // 마스킹 테이프 색
@include stage($paper-sheet);
.tapenote {
box-sizing: border-box;
position: relative;
margin: 0;
width: clamp(224px, 74vw, 308px);
padding: $sp-6 $sp-5 $sp-5;
border-radius: $r-sm;
background: $paper-base;
box-shadow: 0 8px 16px rgba($paper-ink, .14);
transform-origin: 50% -12px; // 규칙8: 종이 밖 축 흔들림(테이프)
animation: noteSway $duration $easing infinite;
}
// 위쪽 두 귀퉁이를 눌러 붙인 마스킹 테이프
.tapenote__tape {
position: absolute;
top: -10px;
width: 56px;
height: $sp-5;
background: $color;
opacity: .92;
border-radius: $r-xs;
}
.tapenote__tape--l { left: $sp-4; transform: rotate(-6deg); }
.tapenote__tape--r { right: $sp-4; transform: rotate(5deg); }
.tapenote__q {
display: block;
color: $paper-coral;
font-size: 22px;
line-height: 1;
margin-bottom: $sp-2;
}
.tapenote__body {
margin: 0 0 $sp-4;
color: $paper-ink;
font-weight: 600; font-size: 15px; line-height: 1.45;
font-family: inherit;
}
.tapenote__by {
display: flex;
align-items: center;
gap: $sp-2;
}
.tapenote__ava {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border-radius: 50%;
background: $paper-leaf;
color: $paper-sheet;
font-weight: 700; font-size: 12px; line-height: 1;
font-family: inherit;
}
.tapenote__who {
color: $paper-stone;
font-weight: 500; font-size: 11px; line-height: 1;
font-family: inherit;
}
// 데모: 테이프 위쪽을 축으로 좌우로 흔들린다. 축이 종이 밖(-12px)이라 아래쪽이 크게 움직인다
@keyframes noteSway {
0%, 100% { transform: rotate(-1.6deg); }
50% { transform: rotate(1.6deg); }
}
@media (max-width: 340px), (max-height: 220px) {
.tapenote { width: 232px; padding: $sp-5 $sp-3 $sp-3; }
.tapenote__tape { width: 40px; height: $sp-4; top: -8px; }
.tapenote__q { font-size: 16px; margin-bottom: $sp-1; }
.tapenote__body { font-size: 12px; margin-bottom: $sp-2; }
.tapenote__ava { width: 20px; height: 20px; font-size: 10px; }
.tapenote__who { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.tapenote { animation: none !important; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.tapenote {
box-sizing: border-box;
position: relative;
margin: 0;
width: clamp(224px, 74vw, 308px);
padding: 24px 20px 20px;
border-radius: 8px;
background: #fbf6ec;
box-shadow: 0 8px 16px rgba(42, 35, 32, 0.14);
transform-origin: 50% -12px;
animation: noteSway 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.tapenote__tape {
position: absolute;
top: -10px;
width: 56px;
height: 20px;
background: #d6c9ab;
opacity: 0.92;
border-radius: 4px;
}
.tapenote__tape--l {
left: 16px;
transform: rotate(-6deg);
}
.tapenote__tape--r {
right: 16px;
transform: rotate(5deg);
}
.tapenote__q {
display: block;
color: #b97b71;
font-size: 22px;
line-height: 1;
margin-bottom: 8px;
}
.tapenote__body {
margin: 0 0 16px;
color: #2a2320;
font-weight: 600;
font-size: 15px;
line-height: 1.45;
font-family: inherit;
}
.tapenote__by {
display: flex;
align-items: center;
gap: 8px;
}
.tapenote__ava {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border-radius: 50%;
background: #2b7d5a;
color: #ffffff;
font-weight: 700;
font-size: 12px;
line-height: 1;
font-family: inherit;
}
.tapenote__who {
color: #5c5048;
font-weight: 500;
font-size: 11px;
line-height: 1;
font-family: inherit;
}
@keyframes noteSway {
0%, 100% {
transform: rotate(-1.6deg);
}
50% {
transform: rotate(1.6deg);
}
}
@media (max-width: 340px), (max-height: 220px) {
.tapenote {
width: 232px;
padding: 20px 12px 12px;
}
.tapenote__tape {
width: 40px;
height: 16px;
top: -8px;
}
.tapenote__q {
font-size: 16px;
margin-bottom: 4px;
}
.tapenote__body {
font-size: 12px;
margin-bottom: 8px;
}
.tapenote__ava {
width: 20px;
height: 20px;
font-size: 10px;
}
.tapenote__who {
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.tapenote {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./TapeNote.module.scss";
export type TapeNoteProps = {
quote?: string;
initial?: string;
who?: string;
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
/** 마스킹 테이프 색 */
color?: string;
/** 좌우로 기우는 각도(deg) */
tilt?: number;
};
type Vars = CSSProperties & {
"--duration"?: string; "--easing"?: string; "--color"?: string; "--tilt"?: string;
};
const QUOTES = (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M108 72v72H40a8 8 0 0 1-8-8V72a8 8 0 0 1 8-8h60a8 8 0 0 1 8 8m108-8h-60a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h68V72a8 8 0 0 0-8-8" opacity=".2"/><path d="M100 56H40a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80H40V72h60Zm116-80h-60a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80h-60V72h60Z"/></g></svg>
);
export default function TapeNote({
quote = "색종이 세 장으로 첫 화면이 끝났습니다.",
initial = "ㅂ",
who = "바이브코더 · 1인 개발",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#f6c453",
tilt = 1.6,
}: TapeNoteProps) {
const vars: Vars = {
"--duration": `${duration}ms`, "--easing": easing, "--color": color, "--tilt": `${tilt}deg`,
};
return (
<figure className={styles.tapenote} style={vars}>
<span className={`${styles.tape} ${styles.tapeL}`} aria-hidden="true" />
<span className={`${styles.tape} ${styles.tapeR}`} aria-hidden="true" />
<span className={styles.q} aria-hidden="true">{QUOTES}</span>
<blockquote className={styles.body}>{quote}</blockquote>
<figcaption className={styles.by}>
<span className={styles.ava} aria-hidden="true">{initial}</span>
<span className={styles.who}>{who}</span>
</figcaption>
</figure>
);
}06 · テープ留めのレビュー — 計測 累積面積 6.52% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
どこを変えるか
回転の軸を紙の外、12px上に置くと、同じ角度でも下端の振れ幅がずっと大きくなり、テープにぶら下がった重さが出ます。
.tapenote {
transform-origin: 50% -12px; /* 軸は紙の外 — 下端が大きく動く */
animation: noteSway 2s cubic-bezier(.2, .8, .2, 1) infinite;
}
.tapenote__tape { position: absolute; top: -10px; width: 56px; height: 20px; opacity: .92; }
.tapenote__tape--l { left: 16px; transform: rotate(-6deg); }
.tapenote__tape--r { right: 16px; transform: rotate(5deg); }
@keyframes noteSway {
0%, 100% { transform: rotate(-1.6deg); }
50% { transform: rotate(1.6deg); }
}
07ちぎれ端のCTA帯
何が見えるか
上下をギザギザにちぎった帯です。ギザギザは画像ではなくマスク3枚で、上下を削るconic-gradientが2枚と本体を残すlinear-gradientが1枚という構成になっていて、動いているのはボタンだけです。

<main class="stage">
<section class="tornband">
<div class="tornband__in">
<p class="tornband__t">今日から切り貼りを始めよう</p>
<button class="tornband__btn" type="button">
<span>パーツを受け取る</span>
<span class="tornband__ic" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m216 128l-72 72V56Z" opacity=".2"/><path d="m221.66 122.34l-72-72A8 8 0 0 0 136 56v64H40a8 8 0 0 0 0 16h96v64a8 8 0 0 0 13.66 5.66l72-72a8 8 0 0 0 0-11.32M152 180.69V75.31L204.69 128Z"/></g></svg>
</span>
</button>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 페이퍼컷 팔레트
$paper-base: #fbf6ec;
$paper-sheet: #ffffff;
$paper-ink: #2a2320;
$paper-stone: #5c5048;
$paper-coral: #b97b71;
$paper-sky: #6a8db5;
$paper-leaf: #2b7d5a;
$paper-butter: #d6c9ab;
$paper-edge: #e7dccb;
$duration: $dur-loop; // 버튼이 떴다 눌리는 한 주기
$easing: $ease-spring; // 종이가 바닥에 닿을 때만 탄력
$color: $paper-coral; // 띠의 색종이 색. 흰 글자 대비 4.93:1
@include stage($paper-base);
.tornband {
box-sizing: border-box;
width: clamp(252px, 84vw, 372px);
padding: $sp-6 $sp-5;
background: $color;
// 위아래를 지그재그로 찢어낸 가장자리 — 마스크 세 겹을 겹쳐 만든다 (애니메이션 대상 아님)
-webkit-mask:
conic-gradient(from -45deg at bottom, #0000, #000 1deg 89deg, #0000 90deg) bottom / 14px 7px repeat-x,
conic-gradient(from 135deg at top, #0000, #000 1deg 89deg, #0000 90deg) top / 14px 7px repeat-x,
linear-gradient(#000 0 0) center / 100% calc(100% - 14px) no-repeat;
mask:
conic-gradient(from -45deg at bottom, #0000, #000 1deg 89deg, #0000 90deg) bottom / 14px 7px repeat-x,
conic-gradient(from 135deg at top, #0000, #000 1deg 89deg, #0000 90deg) top / 14px 7px repeat-x,
linear-gradient(#000 0 0) center / 100% calc(100% - 14px) no-repeat;
}
.tornband__in {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-4;
}
.tornband__t {
margin: 0;
color: $paper-sheet;
font-weight: 800; font-size: clamp(15px, 3.6vw, 19px); line-height: 1.25;
font-family: inherit;
text-align: center;
}
// 종이 버튼 — 그림자가 두께다. 눌리면 두께가 사라진다
.tornband__btn {
display: inline-flex;
align-items: center;
gap: $sp-2;
border: 0;
border-radius: $r-control;
background: $paper-sheet;
color: $paper-ink;
padding: $sp-3 $sp-6;
font-weight: 800; font-size: 14px; line-height: 1;
font-family: inherit;
box-shadow: 0 6px 0 rgba($paper-ink, .3);
animation: paperPress $duration $easing infinite;
}
.tornband__ic { font-size: 16px; line-height: 1; color: $color; }
.tornband__btn:focus-visible { outline: 3px solid $paper-sheet; outline-offset: 3px; }
// 데모: 버튼이 8px 떠올랐다가 바닥에 붙는다. 그림자 두께가 6px→0 으로 같이 줄어든다
@keyframes paperPress {
0%, 100% { transform: translateY(-8px); box-shadow: 0 14px 0 rgba($paper-ink, .3); }
50% { transform: translateY(0); box-shadow: 0 2px 0 rgba($paper-ink, .3); }
}
@media (max-width: 340px), (max-height: 220px) {
.tornband { width: 268px; padding: $sp-5 $sp-3; }
.tornband__in { gap: $sp-3; }
.tornband__t { font-size: 13px; }
.tornband__btn { padding: $sp-2 $sp-4; font-size: 12px; }
.tornband__ic { font-size: 13px; }
}
@media (prefers-reduced-motion: reduce) {
.tornband__btn { animation: none !important; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf6ec;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.tornband {
box-sizing: border-box;
width: clamp(252px, 84vw, 372px);
padding: 24px 20px;
background: #b97b71;
-webkit-mask: conic-gradient(from -45deg at bottom, rgba(0, 0, 0, 0), #000 1deg 89deg, rgba(0, 0, 0, 0) 90deg) bottom/14px 7px repeat-x, conic-gradient(from 135deg at top, rgba(0, 0, 0, 0), #000 1deg 89deg, rgba(0, 0, 0, 0) 90deg) top/14px 7px repeat-x, linear-gradient(#000 0 0) center/100% calc(100% - 14px) no-repeat;
mask: conic-gradient(from -45deg at bottom, rgba(0, 0, 0, 0), #000 1deg 89deg, rgba(0, 0, 0, 0) 90deg) bottom/14px 7px repeat-x, conic-gradient(from 135deg at top, rgba(0, 0, 0, 0), #000 1deg 89deg, rgba(0, 0, 0, 0) 90deg) top/14px 7px repeat-x, linear-gradient(#000 0 0) center/100% calc(100% - 14px) no-repeat;
}
.tornband__in {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.tornband__t {
margin: 0;
color: #ffffff;
font-weight: 800;
font-size: clamp(15px, 3.6vw, 19px);
line-height: 1.25;
font-family: inherit;
text-align: center;
}
.tornband__btn {
display: inline-flex;
align-items: center;
gap: 8px;
border: 0;
border-radius: 12px;
background: #ffffff;
color: #2a2320;
padding: 12px 24px;
font-weight: 800;
font-size: 14px;
line-height: 1;
font-family: inherit;
box-shadow: 0 6px 0 rgba(42, 35, 32, 0.3);
animation: paperPress 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.tornband__ic {
font-size: 16px;
line-height: 1;
color: #b97b71;
}
.tornband__btn:focus-visible {
outline: 3px solid #ffffff;
outline-offset: 3px;
}
@keyframes paperPress {
0%, 100% {
transform: translateY(-8px);
box-shadow: 0 14px 0 rgba(42, 35, 32, 0.3);
}
50% {
transform: translateY(0);
box-shadow: 0 2px 0 rgba(42, 35, 32, 0.3);
}
}
@media (max-width: 340px), (max-height: 220px) {
.tornband {
width: 268px;
padding: 20px 12px;
}
.tornband__in {
gap: 12px;
}
.tornband__t {
font-size: 13px;
}
.tornband__btn {
padding: 8px 16px;
font-size: 12px;
}
.tornband__ic {
font-size: 13px;
}
}
@media (prefers-reduced-motion: reduce) {
.tornband__btn {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./TornBand.module.scss";
export type TornBandProps = {
title?: string;
cta?: string;
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
/** 띠의 색종이 색. 흰 글자와 4.5:1 이상이어야 한다 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const ARROW = (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m216 128l-72 72V56Z" opacity=".2"/><path d="m221.66 122.34l-72-72A8 8 0 0 0 136 56v64H40a8 8 0 0 0 0 16h96v64a8 8 0 0 0 13.66 5.66l72-72a8 8 0 0 0 0-11.32M152 180.69V75.31L204.69 128Z"/></g></svg>
);
export default function TornBand({
title = "오늘 오려 붙이세요",
cta = "부품 받기",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#cc3d26",
}: TornBandProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.tornband} style={vars}>
<div className={styles.in}>
<p className={styles.t}>{title}</p>
<button className={styles.btn} type="button">
<span>{cta}</span>
<span className={styles.ic} aria-hidden="true">{ARROW}</span>
</button>
</div>
</section>
);
}07 · ちぎれ端のCTA帯 — 計測 累積面積 2.563% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードのどこを見るか
ボタンが沈んだ分だけ影の厚みも減ります。
.tornband {
mask:
conic-gradient(from -45deg at bottom, #0000, #000 1deg 89deg, #0000 90deg) bottom / 14px 7px repeat-x,
conic-gradient(from 135deg at top, #0000, #000 1deg 89deg, #0000 90deg) top / 14px 7px repeat-x,
linear-gradient(#000 0 0) center / 100% calc(100% - 14px) no-repeat;
}
.tornband__btn { animation: paperPress 2s cubic-bezier(.2, .8, .2, 1) infinite; }
@keyframes paperPress {
0%, 100% { transform: translateY(-8px); box-shadow: 0 14px 0 rgba(42, 35, 32, .3); }
50% { transform: translateY(0); box-shadow: 0 2px 0 rgba(42, 35, 32, .3); }
}
08色紙ストリップのフッター
画面で起きること
一番下の帯には4色の紙片を2組並べ、1組分だけ流します。一周した瞬間が最初のフレームとピクセル単位で同じになるので、継ぎ目が見えません。

<main class="stage">
<footer class="pfoot">
<div class="pfoot__cols">
<div class="pfoot__col">
<p class="pfoot__h">工作室</p>
<p class="pfoot__l">紹介</p>
<p class="pfoot__l">記録</p>
</div>
<div class="pfoot__col">
<p class="pfoot__h">素材</p>
<p class="pfoot__l">色紙</p>
<p class="pfoot__l">ステッカー</p>
</div>
<div class="pfoot__col">
<p class="pfoot__h">ヘルプ</p>
<p class="pfoot__l">お問い合わせ</p>
<p class="pfoot__l">利用規約</p>
</div>
</div>
<div class="pfoot__strip" aria-hidden="true">
<div class="pfoot__track">
<span class="pfoot__bit pfoot__bit--coral"></span>
<span class="pfoot__bit pfoot__bit--butter"></span>
<span class="pfoot__bit pfoot__bit--leaf"></span>
<span class="pfoot__bit pfoot__bit--sky"></span>
<span class="pfoot__bit pfoot__bit--coral"></span>
<span class="pfoot__bit pfoot__bit--butter"></span>
<span class="pfoot__bit pfoot__bit--leaf"></span>
<span class="pfoot__bit pfoot__bit--sky"></span>
</div>
</div>
</footer>
</main>@use "../../../../site/static/demo-tokens" as *;
// 페이퍼컷 팔레트
$paper-base: #fbf6ec;
$paper-sheet: #ffffff;
$paper-ink: #2a2320;
$paper-stone: #5c5048;
$paper-coral: #b97b71;
$paper-sky: #6a8db5;
$paper-leaf: #2b7d5a;
$paper-butter: #d6c9ab;
$paper-edge: #e7dccb;
$duration: $dur-loop; // 색종이 띠가 한 벌만큼 흘러가는 주기
$easing: linear; // 흐름은 일정 속도라야 이음매가 안 보인다
$color: $paper-edge; // 푸터 종이의 아래 두께
@include stage($paper-butter);
.pfoot {
box-sizing: border-box;
width: clamp(264px, 86vw, 388px);
padding: $sp-5 $sp-5 0;
border-radius: $r-card;
background: $paper-sheet;
box-shadow: 0 6px 0 $color;
overflow: hidden;
}
.pfoot__cols {
display: flex;
gap: $sp-6;
margin-bottom: $sp-4;
}
.pfoot__col { flex: 1 1 0; }
.pfoot__h {
margin: 0 0 $sp-2;
color: $paper-ink;
font-weight: 800; font-size: 12px; line-height: 1;
font-family: inherit;
}
.pfoot__l {
margin: 0 0 $sp-1;
color: $paper-stone;
font-weight: 500; font-size: 11px; line-height: 1.3;
font-family: inherit;
}
// 아래 색종이 띠 — 같은 네 조각을 두 벌 넣고 한 벌만큼(-50%) 민다. 끝에서 처음으로 튀는 순간이 없다
.pfoot__strip {
overflow: hidden;
margin: 0 #{-$sp-5};
padding: $sp-2 0;
background: $paper-base;
}
.pfoot__track {
display: flex;
width: max-content;
animation: stripFlow $duration $easing infinite;
}
.pfoot__bit {
flex: 0 0 auto;
width: 36px;
height: $sp-5;
margin-right: $sp-3; // 간격은 gap 이 아니라 margin — 2N 조각 사이 gap 은 2N-1 개라 -50% 가 어긋난다
border-radius: $r-xs;
}
.pfoot__bit--coral { background: $paper-coral; }
.pfoot__bit--butter { background: $paper-butter; }
.pfoot__bit--leaf { background: $paper-leaf; }
.pfoot__bit--sky { background: $paper-sky; }
// 데모: 트랙 전체가 정확히 한 벌(=전체 폭의 50%)만큼 왼쪽으로 흐른 뒤 처음 상태와 완전히 같아진다
@keyframes stripFlow {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
@media (max-width: 340px), (max-height: 220px) {
.pfoot { width: 276px; padding: $sp-3 $sp-3 0; }
.pfoot__cols { gap: $sp-3; margin-bottom: $sp-2; }
.pfoot__h { font-size: 10px; margin-bottom: $sp-1; }
.pfoot__l { font-size: 9px; }
.pfoot__strip { margin: 0 #{-$sp-3}; padding: $sp-1 0; }
.pfoot__bit { width: 24px; height: $sp-4; margin-right: $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.pfoot__track { animation: none !important; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #d6c9ab;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.pfoot {
box-sizing: border-box;
width: clamp(264px, 86vw, 388px);
padding: 20px 20px 0;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 6px 0 #e7dccb;
overflow: hidden;
}
.pfoot__cols {
display: flex;
gap: 24px;
margin-bottom: 16px;
}
.pfoot__col {
flex: 1 1 0;
}
.pfoot__h {
margin: 0 0 8px;
color: #2a2320;
font-weight: 800;
font-size: 12px;
line-height: 1;
font-family: inherit;
}
.pfoot__l {
margin: 0 0 4px;
color: #5c5048;
font-weight: 500;
font-size: 11px;
line-height: 1.3;
font-family: inherit;
}
.pfoot__strip {
overflow: hidden;
margin: 0 -20px;
padding: 8px 0;
background: #fbf6ec;
}
.pfoot__track {
display: flex;
width: max-content;
animation: stripFlow 2s linear infinite;
}
.pfoot__bit {
flex: 0 0 auto;
width: 36px;
height: 20px;
margin-right: 12px;
border-radius: 4px;
}
.pfoot__bit--coral {
background: #b97b71;
}
.pfoot__bit--butter {
background: #d6c9ab;
}
.pfoot__bit--leaf {
background: #2b7d5a;
}
.pfoot__bit--sky {
background: #6a8db5;
}
@keyframes stripFlow {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
@media (max-width: 340px), (max-height: 220px) {
.pfoot {
width: 276px;
padding: 12px 12px 0;
}
.pfoot__cols {
gap: 12px;
margin-bottom: 8px;
}
.pfoot__h {
font-size: 10px;
margin-bottom: 4px;
}
.pfoot__l {
font-size: 9px;
}
.pfoot__strip {
margin: 0 -12px;
padding: 4px 0;
}
.pfoot__bit {
width: 24px;
height: 16px;
margin-right: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.pfoot__track {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./PaperFooter.module.scss";
export type FooterColumn = { head: string; links: string[] };
export type PaperFooterProps = {
columns?: FooterColumn[];
/** 띠에 놓을 색종이 색 한 벌. 컴포넌트가 두 벌로 복제한다 */
chips?: string[];
/** 한 벌만큼 흐르는 시간(기본 2000ms) */
duration?: number;
};
type Vars = CSSProperties & { "--duration"?: string };
export default function PaperFooter({
columns = [
{ head: "공작실", links: ["소개", "기록"] },
{ head: "재료", links: ["색종이", "스티커"] },
{ head: "도움말", links: ["문의", "약관"] },
],
chips = ["#cc3d26", "#f6c453", "#2b7d5a", "#2f6fb8"],
duration = 2000,
}: PaperFooterProps) {
const vars: Vars = { "--duration": `${duration}ms` };
// 한 벌을 그대로 두 번 깔아야 -50% 지점이 첫 프레임과 완전히 같아진다 (이음매 없음)
const track = [...chips, ...chips];
return (
<footer className={styles.pfoot} style={vars}>
<div className={styles.cols}>
{columns.map((c) => (
<div key={c.head} className={styles.col}>
<p className={styles.h}>{c.head}</p>
{c.links.map((l) => <p key={l} className={styles.l}>{l}</p>)}
</div>
))}
</div>
<div className={styles.strip} aria-hidden="true">
<div className={styles.track}>
{track.map((c, i) => <span key={`${c}-${i}`} className={styles.bit} style={{ background: c }} />)}
</div>
</div>
</footer>
);
}08 · 色紙ストリップのフッター — 計測 累積面積 5.163% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードの読み方
間隔をgapで与えると、この計算が崩れます。
.pfoot__track {
display: flex;
width: max-content;
animation: stripFlow 2s linear infinite;
}
.pfoot__bit {
width: 36px; height: 20px;
margin-right: 12px; /* 2N枚の間のgapは2N-1個しかないので-50%がずれる */
border-radius: 4px;
}
@keyframes stripFlow {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
09紙のボタン・入力欄
どんな動きか
2つのボタンが半周期ずれて6px沈みます。影の6pxがそもそも紙の厚みなので、押し込むと厚みが消えるのが物理的に正しい見え方です。

<main class="stage">
<section class="paperui">
<label class="paperui__field">
<span class="paperui__ic" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m224 56l-96 88l-96-88Z" opacity=".2"/><path d="M224 48H32a8 8 0 0 0-8 8v136a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a8 8 0 0 0-8-8m-20.57 16L128 133.15L52.57 64ZM216 192H40V74.19l82.59 75.71a8 8 0 0 0 10.82 0L216 74.19z"/></g></svg>
</span>
<input class="paperui__input" type="email" placeholder="[email protected]" aria-label="メール">
</label>
<div class="paperui__row">
<button class="paperui__btn paperui__btn--solid" type="button">貼る</button>
<button class="paperui__btn paperui__btn--ghost" type="button">プレビュー</button>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 페이퍼컷 팔레트
$paper-base: #fbf6ec;
$paper-sheet: #ffffff;
$paper-ink: #2a2320;
$paper-stone: #5c5048;
$paper-coral: #b97b71;
$paper-sky: #6a8db5;
$paper-leaf: #2b7d5a;
$paper-butter: #d6c9ab;
$paper-edge: #e7dccb;
$duration: $dur-loop; // 두 버튼이 번갈아 눌리는 한 주기
$easing: $ease-spring; // 종이가 바닥에 닿을 때만 탄력
$color: $paper-coral; // 채운 버튼의 종이색
@include stage($paper-base);
.paperui {
box-sizing: border-box;
width: clamp(240px, 78vw, 328px);
display: flex;
flex-direction: column;
gap: $sp-4;
padding: $sp-5;
border-radius: $r-card;
background: $paper-sheet;
box-shadow: 0 6px 0 $paper-edge;
}
// 인풋 — 종이에 홈을 판 것처럼 안쪽 그림자
.paperui__field {
display: flex;
align-items: center;
gap: $sp-2;
padding: $sp-3 $sp-4;
border-radius: $r-control;
background: $paper-base;
box-shadow: inset 0 3px 0 $paper-edge;
}
.paperui__ic { color: $paper-stone; font-size: 16px; line-height: 1; }
.paperui__input {
flex: 1 1 auto;
min-width: 0;
border: 0;
background: transparent;
color: $paper-ink;
font-weight: 500; font-size: 13px; line-height: 1;
font-family: inherit;
}
.paperui__input::placeholder { color: $paper-stone; }
.paperui__input:focus-visible { outline: 2px solid $paper-sky; outline-offset: 4px; }
.paperui__row { display: flex; gap: $sp-3; }
.paperui__btn {
flex: 1 1 0;
border: 0;
border-radius: $r-control;
padding: $sp-3 $sp-4;
font-weight: 800; font-size: 13px; line-height: 1;
font-family: inherit;
animation: btnPress $duration $easing infinite;
}
.paperui__btn--solid {
background: $color;
color: $paper-sheet;
box-shadow: 0 6px 0 rgba($paper-ink, .28);
}
.paperui__btn--ghost {
background: $paper-sheet;
color: $paper-ink;
box-shadow: 0 6px 0 $paper-edge, inset 0 0 0 2px $paper-edge;
animation-delay: -1s; // 반 주기 뒤 — 둘이 번갈아 눌린다
}
.paperui__btn:focus-visible { outline: 3px solid $paper-sky; outline-offset: 3px; }
// 데모: 버튼이 종이 두께(그림자 6px)만큼 내려앉았다가 되돌아온다
@keyframes btnPress {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(6px); }
}
@media (max-width: 340px), (max-height: 220px) {
.paperui { width: 256px; padding: $sp-3; gap: $sp-3; }
.paperui__field { padding: $sp-2 $sp-3; }
.paperui__ic { font-size: 13px; }
.paperui__input { font-size: 11px; }
.paperui__row { gap: $sp-2; }
.paperui__btn { padding: $sp-2 $sp-3; font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.paperui__btn { animation: none !important; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf6ec;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.paperui {
box-sizing: border-box;
width: clamp(240px, 78vw, 328px);
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 6px 0 #e7dccb;
}
.paperui__field {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-radius: 12px;
background: #fbf6ec;
box-shadow: inset 0 3px 0 #e7dccb;
}
.paperui__ic {
color: #5c5048;
font-size: 16px;
line-height: 1;
}
.paperui__input {
flex: 1 1 auto;
min-width: 0;
border: 0;
background: transparent;
color: #2a2320;
font-weight: 500;
font-size: 13px;
line-height: 1;
font-family: inherit;
}
.paperui__input::placeholder {
color: #5c5048;
}
.paperui__input:focus-visible {
outline: 2px solid #6a8db5;
outline-offset: 4px;
}
.paperui__row {
display: flex;
gap: 12px;
}
.paperui__btn {
flex: 1 1 0;
border: 0;
border-radius: 12px;
padding: 12px 16px;
font-weight: 800;
font-size: 13px;
line-height: 1;
font-family: inherit;
animation: btnPress 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.paperui__btn--solid {
background: #b97b71;
color: #ffffff;
box-shadow: 0 6px 0 rgba(42, 35, 32, 0.28);
}
.paperui__btn--ghost {
background: #ffffff;
color: #2a2320;
box-shadow: 0 6px 0 #e7dccb, inset 0 0 0 2px #e7dccb;
animation-delay: -1s;
}
.paperui__btn:focus-visible {
outline: 3px solid #6a8db5;
outline-offset: 3px;
}
@keyframes btnPress {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(6px);
}
}
@media (max-width: 340px), (max-height: 220px) {
.paperui {
width: 256px;
padding: 12px;
gap: 12px;
}
.paperui__field {
padding: 8px 12px;
}
.paperui__ic {
font-size: 13px;
}
.paperui__input {
font-size: 11px;
}
.paperui__row {
gap: 8px;
}
.paperui__btn {
padding: 8px 12px;
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.paperui__btn {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./PaperUI.module.scss";
export type PaperUIProps = {
placeholder?: string;
solidLabel?: string;
ghostLabel?: string;
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
/** 채운 버튼의 종이색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const MAIL = (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m224 56l-96 88l-96-88Z" opacity=".2"/><path d="M224 48H32a8 8 0 0 0-8 8v136a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a8 8 0 0 0-8-8m-20.57 16L128 133.15L52.57 64ZM216 192H40V74.19l82.59 75.71a8 8 0 0 0 10.82 0L216 74.19z"/></g></svg>
);
export default function PaperUI({
placeholder = "[email protected]",
solidLabel = "붙이기",
ghostLabel = "미리보기",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#cc3d26",
}: PaperUIProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.paperui} style={vars}>
<label className={styles.field}>
<span className={styles.ic} aria-hidden="true">{MAIL}</span>
<input className={styles.input} type="email" placeholder={placeholder} aria-label="이메일" />
</label>
<div className={styles.row}>
<button className={`${styles.btn} ${styles.solid}`} type="button">{solidLabel}</button>
<button className={`${styles.btn} ${styles.ghost}`} type="button">{ghostLabel}</button>
</div>
</section>
);
}09 · 紙のボタン・入力欄 — 計測 累積面積 4.483% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
どこを変えるか
入力欄は逆に、内側の影で彫った溝にします。
.paperui__field {
box-shadow: inset 0 3px 0 #e7dccb; /* 彫った溝 — 外側の影を一緒に付けない */
}
.paperui__btn--solid { box-shadow: 0 6px 0 rgba(42, 35, 32, .28); }
.paperui__btn--ghost {
box-shadow: 0 6px 0 #e7dccb, inset 0 0 0 2px #e7dccb;
animation-delay: -1s; /* 半周期あと — 2つが交互に沈む */
}
@keyframes btnPress {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(6px); }
}
tailwind.config カスタム
このスタイルで1か所にまとめるべきなのは、色ではなく影です。床に付いた紙の厚み(shadow-paper)と、持ち上げたときのぼけた影(shadow-lift)を混ぜて使うと紙に見えなくなるので、名前をつける段階で分けておきます。
theme: {
extend: {
colors: {
paper: "#fbf6ec", sheet: "#ffffff", ink: "#2a2320", stone: "#5c5048",
coral: "#cc3d26", sky: "#2f6fb8", leaf: "#2b7d5a",
butter: "#f6c453", blush: "#f6b3ab", edge: "#e7dccb",
},
borderRadius: { card: "16px", control: "12px", chip: "4px", pill: "999px" },
boxShadow: {
paper: "0 6px 0 rgba(42,35,32,.16)", // 床に付いた紙 = 厚み
"paper-edge": "0 6px 0 #e7dccb",
lift: "0 18px 8px rgba(42,35,32,.12)", // 持ち上げたときだけ
"lift-high": "0 22px 28px rgba(42,35,32,.1)",
groove: "inset 0 3px 0 #e7dccb", // 彫った溝
},
},
},
| 役割 | トークン | いつ |
|---|---|---|
| 紙の厚み | shadow-paper |
カードやボタンが床に付いているとき |
| 持ち上げ | shadow-lift |
ホバー・アクティブのときだけ |
| 彫った溝 | shadow-groove |
入力欄の内側 |
バイブコーディング プロンプト
設定を入れたあと、Claude CodeやCursorに渡す指示は次のひとかたまりで足ります。色のリストより影のルールを先に置く、という順番が大事です。省略していない原文はzipの中のprompt.mdに入っています。
床に付いた紙の影はぼかしを0にする: 0 6px 0 rgba(42,35,32,.16)。この6pxが紙の厚みで、
画面全体で同じ値にする。持ち上げるときだけ 0 18px 8px のぼけた影に変える。背景は
paper(#fbf6ec)の1トーン、面は sheet(#ffffff)の1トーン。色紙は5色で、1画面に3色まで。
白い文字は coral・sky・leaf の上にだけ置く。ちぎれた端は画像ではなく conic-gradient の
マスクで作る。
この指示どおりにpage/index.htmlを作り、1280pxのビューポートでページ全体を撮ったものが下の画像です。

どこで壊れるか — 落とし穴
一番長く気づけなかったのは文字の大きさでした。9個のパーツのスタイルにfont: 800 24px/1.1;のように大きさと太さを1行にまとめ、すぐ下にfont-family: inherit;を置いていたのですが、レンダリングした画面では料金の数字が見出しより小さく見えました。Playwrightで算出値を直接取ってみると、その数字は24pxではなく16px/400、ヒーローの見出しは狙った22pxではなくブラウザ既定の32px/700でした。fontのショートハンドは最後の位置にfont-familyが必ず必要で、そこが空だと宣言そのものが捨てられます。9つのファイルに散らばった21か所をfont-weight・font-size・line-heightの個別プロパティに分解して、ようやく測った値と書いた値が一致しました。
2つ目はワンページ側でした。Tailwindのplay CDNを使いながら、普通の<style>の中にbody { background: theme('colors.paper') }と書いたところ、画面はただの白で、料金カードの折れた角はまったく見えませんでした。算出値の背景色を取るとどちらもrgba(0, 0, 0, 0)——play CDNはtype="text/tailwindcss"が付いたstyleだけをコンパイルし、それ以外の<style>の中のtheme()は無効として捨てます。切り抜きの跡を生かす影の重ね方はメール画面に移してもそのまま動き、メールテンプレート デザインがクライアント互換の基準まで抑えています。この2つを直した完成版は、解凍パスワードrysqfgwrで開くzipにそのまま入っています。
アクセシビリティ
明るい紙のトーンはコントラストを確保しやすい方ですが、色紙の上に白い文字を載せた瞬間に基準線ぎりぎりになります。WCAG 2.1 最小コントラスト基準の相対輝度の式で実際に計算してみると、インク(#2a2320)は紙の地(#fbf6ec)に対して14.34:1、補助文字のストーン(#5c5048)は7.23:1で、本文の基準4.5:1を十分に超えます。問題はアクセント色でした。最初に選んだコーラル#d6452fは白文字に対して4.42:1と基準を下回り、#cc3d26まで暗くしてやっと4.93:1になりました。同じやり方でスカイ(#2f6fb8)は5.14:1、リーフ(#2b7d5a)は5.02:1を確保し、バター(#f6c453)とブラッシュ(#f6b3ab)は白文字ではどうしても基準に届かないため、インク文字だけを載せる(それぞれ9.52:1、8.79:1)ようにコード側で制限しました。
動きについては、MDN prefers-reduced-motion のページのメディアクエリを9個すべてに入れ、動きを減らす設定にしているユーザーには紙が浮いたり帯が流れたりせず、静止した状態だけが見えるようにしました。そのときもMDN box-shadow のページどおりに描いた厚みの影は残るので、層の構造は読み取れます。ちぎれ端に使ったMDN mask のページのマスクは装飾なので、対応していないブラウザでは四角い帯に見えるだけで、内容は失われません。
ほかのWebデザインの流行が気になる方はデザイントレンドのカテゴリをご覧ください。このブログが何を配っているのかは紹介ページにまとめてあります。
FAQ
紙 っ ぽい web デザインには紙のテクスチャ画像を敷いた方がそれらしくなりませんか?
テクスチャ写真を敷くとファイルが重くなり、背景色を変えるたびに画像を作り直すことになります。このキットが紙に見える理由は質感ではなく、ぼかし0の影と一方向にそろえた厚みです。写真なしで層が読み取れるようになれば、そこから先の質感は好みで足せる選択肢のひとつになります。点と波で同じ手触りを出す隣の軸はメンフィス デザイン パターンです。
影の厚みを要素ごとに変えてもいいですか?
1つの画面では同じ値にそろえる方が安全です。厚みがばらばらだと、見る人はそれを「紙の厚さの違い」ではなく「そろえ忘れ」として受け取ります。厚みを変えたいときは要素ごとに散らさず、6pxから3pxあるいは10pxへ、画面全体を一度に動かしてください。
暗い背景でもこのスタイルは成立しますか?
厚みの影は背景より暗くないと層が見えませんが、暗い背景ではその余地がなくなります。どうしても使うなら、影を濃くするよりも、各紙の上端に明るい線を1本(inset 0 1px 0)入れて光の当たる面を作る方が現実的です。ただしその時点で、このスタイルの明るく軽い印象は消えます。









