Paper Cutout Design: 9 Layered Landing Sections
Paper cutout design is a layout style where every surface looks like it was cut with scissors and glued down: each panel gets its thickness from a zero-blur
Most "paper cutout" boards on Pinterest are Photoshop exports, and they get heavy the moment you move them to the web. Nothing that follows uses a single image file. Scroll the one-pager first and see if you can spot what actually makes it read as paper.

Below the page, the nine parts it's built from are broken out into their own grid. The order isn't by popularity — this paper cutting design follows what happens to a single sheet of paper on screen: it gets cut and placed, moved, stacked, stamped, dog-eared, taped, torn, sent drifting, and finally pressed. On the same kit axis, the gloss-free whitespace-first version is SaaS Landing Page Design, and the direction that stacks even more texture is Maximalist Web Design.
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
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Cut-paper hero
- 02 Lifted paper tab nav
- 03 Paper-band card grid
- 04 Sticker checkmark list
- 05 Dog-ear pricing
- 06 Taped testimonial note
- 07 Torn-edge CTA band
- 08 Confetti strip footer
- 09 Paper buttons & inputs
01Cut-paper hero
What you see in this collage web design
A white sheet rises while the three colored scraps behind it sink. The two layers have to move in opposite directions, or the stack reads as one flat plane.

<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">A cut-and-paste screen</h1>
<p class="heropaper__desc">A landing page, a few sheets of paper</p>
<button class="heropaper__cta" type="button">Get started</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 · Cut-paper hero — Measured area 14.784% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where to look in the code
The shadow drops by exactly as much as the sheet lifts, blurring and lightening on the way.
.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); }
}
02Lifted paper tab nav
What happens on screen
A yellow paper chip hops between tabs instead of an underline sliding underneath.

<main class="stage">
<nav class="navpaper" aria-label="Demo navigation">
<span class="navpaper__brand">Craft Room</span>
<div class="navpaper__tabs">
<span class="navpaper__chip" aria-hidden="true"></span>
<span class="navpaper__tab">Home</span>
<span class="navpaper__tab">Materials</span>
<span class="navpaper__tab">Pricing</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 · Lifted paper tab nav — Measured area 5.53% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
How to read the code
Because it rises 5px before every move, it reads as being picked up and set back down rather than sliding across.
.navpaper__tabs { --step: 76px; } /* 72px tab + 4px gap */
.navpaper__tab { flex: 0 0 72px; } /* fixed width, so the hop distance matches the render */
.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); }
}
03Paper-band card grid
What moves
Each of three white cards wears a different paper band across its top edge. The card bodies stay white; color only ever appears in the band and the icon.

<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">Layering</h2>
<p class="cardpaper__d">Depth comes in layers</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">Coloring</h2>
<p class="cardpaper__d">Only three colors</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">Pasting</h2>
<p class="cardpaper__d">Pinned with a shadow</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 · Paper-band card grid — Measured area 17.899% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
What to change
They peel up in turn, 0.22s apart.
.cardpaper { display: flex; gap: 12px; align-items: stretch; }
.cardpaper__card {
box-shadow: 0 6px 0 rgba(42, 35, 32, .16); /* zero blur = paper thickness */
animation: cardPeel 2s cubic-bezier(.2, .8, .2, 1) infinite;
}
.cardpaper__card::before { /* the band laid across the top */
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); }
}
04Sticker checkmark list
What you see
Four round stickers get stamped down in sequence. The weight of the press comes from squashing to 0.45, overshooting to 1.18, and then settling.

<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">No more than three colors</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">One shadow direction</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">Corners share one radius</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">Text in a single ink color</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 · Sticker checkmark list — Measured area 3.438% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where to look in the code
With four stickers and four colors, each one gets a color of its own.
.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); }
}
05Dog-ear pricing
What happens on screen
The recommended plan's top-right corner folds over, showing the sheet's back-side color, and then opens back up.

<main class="stage">
<section class="dogear">
<article class="dogear__plan">
<p class="dogear__name">Single sheet</p>
<p class="dogear__price">₩0</p>
<p class="dogear__note">1 piece</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">Bundle</p>
<p class="dogear__price">₩9<span>,900</span></p>
<p class="dogear__note">Bundle of 9 pieces</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 · Dog-ear pricing — Measured area 0.525% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
How to read the code
The triangle is cut once with clip-path, and the fold itself is a single scale pinned to the corner point.
.dogear__plan { position: relative; overflow: hidden; }
.dogear__fold {
position: absolute; top: 0; right: 0; width: 40px; height: 40px;
background: #f6c453; /* the back side of the sheet */
clip-path: polygon(100% 0, 0 0, 100% 100%);
transform-origin: 100% 0; /* fold around the corner point */
animation: cornerFold 2s ease-out infinite;
}
@keyframes cornerFold {
0%, 100% { transform: scale(1); }
50% { transform: scale(.28); }
}
06Taped testimonial note
What moves
Two strips of masking tape hold down the note's top corners.

<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">Three sheets of paper, and the hero is done.</blockquote>
<figcaption class="tapenote__by">
<span class="tapenote__ava" aria-hidden="true">ㅂ</span>
<span class="tapenote__who">Vibe coder · solo builder</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 · Taped testimonial note — Measured area 6.52% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password
What to change
Putting the pivot 12px above the paper means the same rotation swings the bottom edge much farther, which is what sells the weight of the hanging note.
.tapenote {
transform-origin: 50% -12px; /* pivot outside the sheet */
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); }
}
07Torn-edge CTA band
What you see
The band is torn into a zigzag along both long edges. The teeth come from three mask layers rather than an image: two conic-gradient layers that bite into the top and bottom edges, plus one linear-gradient that keeps the body of the band intact.

<main class="stage">
<section class="tornband">
<div class="tornband__in">
<p class="tornband__t">Start cutting and pasting today</p>
<button class="tornband__btn" type="button">
<span>Get the pieces</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 · Torn-edge CTA band — Measured area 2.563% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where to look in the code
The only thing moving is the button, and its shadow thins as it drops.
.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); }
}
08Confetti strip footer
What happens on screen
The closing strip holds two identical sets of four chips and slides by exactly one set. When the cycle wraps, the last frame is pixel-for-pixel identical to the first, so there's no seam.

<main class="stage">
<footer class="pfoot">
<div class="pfoot__cols">
<div class="pfoot__col">
<p class="pfoot__h">Craft Room</p>
<p class="pfoot__l">About</p>
<p class="pfoot__l">Blog</p>
</div>
<div class="pfoot__col">
<p class="pfoot__h">Materials</p>
<p class="pfoot__l">Paper</p>
<p class="pfoot__l">Stickers</p>
</div>
<div class="pfoot__col">
<p class="pfoot__h">Help</p>
<p class="pfoot__l">Contact</p>
<p class="pfoot__l">Terms</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 · Confetti strip footer — Measured area 5.163% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
How to read the code
Spacing the chips with gap breaks that arithmetic.
.pfoot__track {
display: flex;
width: max-content;
animation: stripFlow 2s linear infinite;
}
.pfoot__bit {
width: 36px; height: 20px;
margin-right: 12px; /* 2N chips have only 2N-1 gaps, so -50% would drift */
border-radius: 4px;
}
@keyframes stripFlow {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
09Paper buttons & inputs
What moves
Two buttons press down 6px, half a cycle apart. Since that 6px shadow is the paper's thickness, each press sinks the button by exactly one sheet.

<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="Email">
</label>
<div class="paperui__row">
<button class="paperui__btn paperui__btn--solid" type="button">Pasting</button>
<button class="paperui__btn paperui__btn--ghost" type="button">Preview</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 · Paper buttons & inputs — Measured area 4.483% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password
What to change
The input flips the idea around with an inset shadow.
.paperui__field {
box-shadow: inset 0 3px 0 #e7dccb; /* a carved groove — never add an outer shadow too */
}
.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; /* half a cycle behind, so they alternate */
}
@keyframes btnPress {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(6px); }
}
The custom tailwind.config
What has to live in one place here isn't the palette but the shadows. Mixing the thickness of a sheet lying flat (shadow-paper) with the blurred shadow of a lifted one (shadow-lift) is what breaks the paper illusion, so the two get separate names from the start.
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)", // flat on the surface = thickness
"paper-edge": "0 6px 0 #e7dccb",
lift: "0 18px 8px rgba(42,35,32,.12)", // only while raised
"lift-high": "0 22px 28px rgba(42,35,32,.1)",
groove: "inset 0 3px 0 #e7dccb", // carved groove
},
},
},
| Role | Token | When |
|---|---|---|
| Paper thickness | shadow-paper |
A card or button resting on the surface |
| Raised | shadow-lift |
Hover and active states only |
| Carved groove | shadow-groove |
Inside an input |
The vibe-coding prompt
With the config in place, the instruction you hand Cursor or Claude Code fits in one block. What matters is that the shadow rules come before the color list. The full version ships as prompt.md inside the zip.
Give paper lying flat a zero-blur shadow: 0 6px 0 rgba(42,35,32,.16). Those 6px are the
paper's thickness and stay identical across the whole page. Swap to 0 18px 8px only while
something is raised. Background is one tone, paper(#fbf6ec); surfaces are one tone,
sheet(#ffffff). There are five paper colors and at most three per screen. White text goes
on coral, sky, or leaf only. Build torn edges from conic-gradient masks, never an image.
Here's a full-page capture of page/index.html at a 1280px viewport, built from exactly that instruction.

Where it breaks (the trap)
The bug that hid longest was type size. Across all nine stylesheets, size and weight were packed onto one line as font: 800 24px/1.1; with font-family: inherit; right underneath — and in the render, the price figure looked smaller than the heading above it. Dumping the computed values with Playwright showed that figure resolving to 16px/400 instead of 24px, and the hero heading landing on the browser default of 32px/700 rather than the intended 22px. The font shorthand requires a font-family in its final slot; leave it out and the browser discards the entire declaration. Only after unpacking all 21 occurrences into separate font-weight, font-size, and line-height properties did the measured values match the written ones.
The second one lived in the one-pager. With the Tailwind Play CDN in use, body { background: theme('colors.paper') } sat in an ordinary <style> block — and the page rendered plain white while the dog-ear on the pricing card never showed up at all. Both computed backgrounds came back as rgba(0, 0, 0, 0): the Play CDN compiles only a <style> tag marked type="text/tailwindcss" and throws away theme() anywhere else. The stacked shadows that keep the cut edges readable carry over to the inbox, where Transactional Email Templates adds the client-compatibility bar. The build with both problems fixed is in the zip, which opens with the password rysqfgwr.
Accessibility
Bright paper tones make contrast easy to reason about, right up until white text lands on a colored scrap. Working the relative-luminance formula from the WCAG 2.1 minimum contrast criterion by hand puts ink (#2a2320) at 14.34:1 against the paper ground (#fbf6ec) and the secondary stone (#5c5048) at 7.23:1, both comfortably past the 4.5:1 body-text bar. The accents were the problem. The coral originally picked, #d6452f, came out at 4.42:1 against white — under the bar — and had to be darkened to #cc3d26 before it reached 4.93:1. The same pass gave sky (#2f6fb8) 5.14:1 and leaf (#2b7d5a) 5.02:1, while butter (#f6c453) and blush (#f6b3ab) couldn't carry white text at any usable value, so the code restricts them to ink text at 9.52:1 and 8.79:1.
For motion, the media query from the MDN prefers-reduced-motion page is wired into all nine parts, so visitors who reduce motion see the sheets and the strip held still. The thickness shadows, which follow the MDN box-shadow page, survive that, so the layering still reads. The torn edge, built with the technique on the MDN mask page, is pure decoration: a browser without support just shows a rectangular band, and no content is lost.
For other landing page styles, browse the design trends category; what this blog gives away is explained on the about page.
FAQ
Wouldn't a paper texture image sell these paper cutout design ideas better?
A texture photo makes the page heavier and forces a re-export every time the background color changes. What sells paper here is the zero-blur shadow and a thickness that always points the same way. Once the layers read without a photo, texture becomes optional rather than load-bearing. The neighbor that gets the same hand feel from dots and waves is Memphis Design Patterns.
Can each element get its own shadow thickness?
Keeping one value per screen is safer. When thicknesses vary, people read it as misalignment rather than as sheets of different weights. If you want a different feel, move the whole page from 6px to 3px or 10px at once instead of scattering values element by element.
Does this style survive on a dark background?
The thickness shadow has to be darker than the ground to register, and a dark ground leaves it nowhere to go. If you have to, drop the shadow and give each sheet a bright top edge instead (inset 0 1px 0) so it catches the light. That trade gives up the airy, weightless feel this style is built on.









