Maximalist Web Design: 9 Collage Landing Sections
Maximalist web design fills the screen instead of emptying it, stacking color fields, oversized type, and stickers until the page feels full.
Boards full of these screens are easy to save and hard to start. The problem is never the number of elements — it's that nothing decides how they overlap. Scroll the one-pager below and count how many things were actually chosen by hand. Three.

The grid below pulls that page apart into its nine parts. The order isn't by popularity — in maximalism web design it's the order you'd actually assemble a collage in: lay the frames, add the index, arrange the clippings, press the stamps, wrap the bands, draw the underline, stack the cards, cut the edge, and sign the bottom. Among sibling landing kits, the one that solves layering with paper is Paper Cutout Design, and the one that pushes texture all the way to touch is Skeuomorphism Design Kit.
01 hero-collage
02 nav-collage
03 card-grid-collage
04 sticker-badges-collage
05 marquee-band-collage
06 testimonial-collage
07 pricing-collage
08 cta-band-collage
09 footer-collage
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Stacked frame hero
- 02 Magazine index nav
- 03 Cut-out frame card grid
- 04 Rotated stamp badges
- 05 Marquee text band
- 06 Hand-drawn underline quote
- 07 Overlapping pricing layers
- 08 Stamp CTA band
- 09 Collage footer
01Stacked frame hero
What this collage web design shows
Three gradient-filled photo frames sway out of phase with one another, and only the title card pasted over them rises.

<main class="stage">
<section class="hcol">
<span class="hcol__frame hcol__frame--coral" aria-hidden="true"></span>
<span class="hcol__frame hcol__frame--sky" aria-hidden="true"></span>
<span class="hcol__frame hcol__frame--lime" aria-hidden="true"></span>
<div class="hcol__card">
<p class="hcol__eyebrow">ISSUE 04</p>
<h1 class="hcol__title">A cut-and-paste edit</h1>
<p class="hcol__desc">Screens that overlap and tilt</p>
<button class="hcol__cta" type="button">See it now</button>
</div>
<span class="hcol__stamp" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M40.2,95.8a28,28,0,1,1,39.6,0A28,28,0,0,1,40.2,95.8Zm0,64.4a28,28,0,1,0,39.6,0A28,28,0,0,0,40.2,160.2Z" opacity="0.2"/><path d="M157.73,113.13A8,8,0,0,1,159.82,102L227.48,55.7a8,8,0,0,1,9,13.21l-67.67,46.3a7.92,7.92,0,0,1-4.51,1.4A8,8,0,0,1,157.73,113.13Zm80.87,85.09a8,8,0,0,1-11.12,2.08L136,137.7,93.49,166.78a36,36,0,1,1-9-13.19L121.83,128,84.44,102.41a35.86,35.86,0,1,1,9-13.19l143,97.87A8,8,0,0,1,238.6,198.22ZM80,180a20,20,0,1,0-5.86,14.14A19.85,19.85,0,0,0,80,180ZM74.14,90.13a20,20,0,1,0-28.28,0A19.85,19.85,0,0,0,74.14,90.13Z"/></svg>
</span>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 콜라주·맥시멀 팔레트 — 크림 바탕 한 장 위에 오려 붙인 상큼한 다섯 색.
// 대비 실측(WCAG 상대휘도 직접 계산): ink/cream 14.76:1 · slate/cream 6.90:1 · sheet/coral 4.59:1
$cg-cream: #fbf3e4;
$cg-sheet: #ffffff;
$cg-ink: #241d2e;
$cg-slate: #57506a;
$cg-coral: #bb7f74;
$cg-sky: #668cb4;
$cg-lime: #bfce9c;
$cg-lemon: #d1c8a3;
$serif: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
$duration: $dur-loop; // 프레임 한 장이 흔들렸다 돌아오는 한 주기
$easing: $ease-spring; // 종이는 붙어 있으니 끝에서 살짝 눌린다
$color: $cg-coral; // 맨 뒤 프레임의 대표색
@include stage($cg-cream);
.hcol {
position: relative;
width: clamp(196px, 62vw, 268px);
isolation: isolate;
}
// 뒤에 오려 붙인 사진 프레임 — 흰 여백 테두리 + 색면(사진 자리)
.hcol__frame {
position: absolute;
z-index: 0;
border: 6px solid $cg-sheet;
border-radius: $r-xs;
box-shadow: 0 4px 0 rgba($cg-ink, .16);
animation-name: frameSway;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.hcol__frame--coral {
--rot: -9deg;
left: -30px;
top: -14px;
width: 72px;
height: 88px;
background: linear-gradient(150deg, $cg-coral 0%, $cg-lemon 100%);
}
.hcol__frame--sky {
--rot: 8deg;
right: -28px;
top: -22px;
width: 64px;
height: 76px;
background: linear-gradient(200deg, $cg-sky 0%, $cg-lime 100%);
animation-delay: -.7s;
}
.hcol__frame--lime {
--rot: -5deg;
right: -16px;
bottom: -26px;
width: 88px;
height: 52px;
background: linear-gradient(90deg, $cg-lime 0%, $cg-sky 100%);
animation-delay: -1.4s;
}
// 맨 위에 붙인 제목 카드 — 층이 하나 더 있다는 걸 그림자 폭으로 알린다
.hcol__card {
position: relative;
z-index: 2;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $sp-2;
padding: $sp-4 $sp-5;
border-radius: $r-xs;
background: $cg-sheet;
box-shadow: 0 6px 0 rgba($cg-ink, .18);
animation-name: cardRise;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.hcol__eyebrow {
margin: 0;
color: $cg-coral;
font-weight: 700;
font-size: clamp(9px, 2.4vw, 11px);
line-height: 1;
letter-spacing: .18em;
}
.hcol__title {
margin: 0;
color: $cg-ink;
font-family: $serif;
font-weight: 700;
font-size: clamp(20px, 5.4vw, 30px);
line-height: 1.1;
}
.hcol__desc {
margin: 0;
color: $cg-slate;
font-weight: 500;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1.4;
}
.hcol__cta {
border: 0;
border-radius: $r-pill;
background: $cg-ink;
color: $cg-sheet;
padding: $sp-2 $sp-4;
font-family: inherit;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 12px);
line-height: 1;
}
.hcol__cta:focus-visible {
outline: 3px solid $cg-sky;
outline-offset: 2px;
}
// 기울여 붙인 스탬프 — 맨 위 층이라 카드보다 앞
.hcol__stamp {
position: absolute;
z-index: 3;
right: -22px;
top: -26px;
width: 40px;
aspect-ratio: 1;
display: grid;
place-items: center;
border-radius: 50%;
background: $cg-lemon;
color: $cg-ink;
font-size: 20px;
transform: rotate(-14deg);
box-shadow: 0 4px 0 rgba($cg-ink, .2);
}
// 데모: 뒤 프레임 세 장은 서로 다른 위상으로 흔들리고, 앞 카드만 제자리에서 뜬다
@keyframes frameSway {
0%, 100% { transform: translateY(0) rotate(var(--rot)); }
50% { transform: translateY(8px) rotate(calc(var(--rot) + 5deg)); }
}
@keyframes cardRise {
0%, 100% { transform: translateY(0); box-shadow: 0 6px 0 rgba($cg-ink, .18); }
50% { transform: translateY(-10px); box-shadow: 0 16px 0 rgba($cg-ink, .12); }
}
@media (max-width: 360px), (max-height: 230px) {
.hcol { width: 168px; }
.hcol__card { padding: $sp-3; gap: $sp-1; }
.hcol__desc { display: none; }
.hcol__title { font-size: 18px; }
.hcol__frame--coral { width: 48px; height: 60px; left: -20px; top: -10px; }
.hcol__frame--sky { width: 44px; height: 52px; right: -18px; top: -14px; }
.hcol__frame--lime { width: 56px; height: 36px; right: -12px; bottom: -16px; }
.hcol__stamp { width: 28px; font-size: 14px; right: -14px; top: -16px; }
}
@media (prefers-reduced-motion: reduce) {
.hcol__frame, .hcol__card { animation: none !important; }
.hcol__frame--coral { transform: rotate(-9deg); }
.hcol__frame--sky { transform: rotate(8deg); }
.hcol__frame--lime { transform: rotate(-5deg); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf3e4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.hcol {
position: relative;
width: clamp(196px, 62vw, 268px);
isolation: isolate;
}
.hcol__frame {
position: absolute;
z-index: 0;
border: 6px solid #ffffff;
border-radius: 4px;
box-shadow: 0 4px 0 rgba(36, 29, 46, 0.16);
animation-name: frameSway;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.hcol__frame--coral {
--rot: -9deg;
left: -30px;
top: -14px;
width: 72px;
height: 88px;
background: linear-gradient(150deg, #bb7f74 0%, #d1c8a3 100%);
}
.hcol__frame--sky {
--rot: 8deg;
right: -28px;
top: -22px;
width: 64px;
height: 76px;
background: linear-gradient(200deg, #668cb4 0%, #bfce9c 100%);
animation-delay: -0.7s;
}
.hcol__frame--lime {
--rot: -5deg;
right: -16px;
bottom: -26px;
width: 88px;
height: 52px;
background: linear-gradient(90deg, #bfce9c 0%, #668cb4 100%);
animation-delay: -1.4s;
}
.hcol__card {
position: relative;
z-index: 2;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 16px 20px;
border-radius: 4px;
background: #ffffff;
box-shadow: 0 6px 0 rgba(36, 29, 46, 0.18);
animation-name: cardRise;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.hcol__eyebrow {
margin: 0;
color: #bb7f74;
font-weight: 700;
font-size: clamp(9px, 2.4vw, 11px);
line-height: 1;
letter-spacing: 0.18em;
}
.hcol__title {
margin: 0;
color: #241d2e;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: clamp(20px, 5.4vw, 30px);
line-height: 1.1;
}
.hcol__desc {
margin: 0;
color: #57506a;
font-weight: 500;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1.4;
}
.hcol__cta {
border: 0;
border-radius: 999px;
background: #241d2e;
color: #ffffff;
padding: 8px 16px;
font-family: inherit;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 12px);
line-height: 1;
}
.hcol__cta:focus-visible {
outline: 3px solid #668cb4;
outline-offset: 2px;
}
.hcol__stamp {
position: absolute;
z-index: 3;
right: -22px;
top: -26px;
width: 40px;
aspect-ratio: 1;
display: grid;
place-items: center;
border-radius: 50%;
background: #d1c8a3;
color: #241d2e;
font-size: 20px;
transform: rotate(-14deg);
box-shadow: 0 4px 0 rgba(36, 29, 46, 0.2);
}
@keyframes frameSway {
0%, 100% {
transform: translateY(0) rotate(var(--rot));
}
50% {
transform: translateY(8px) rotate(calc(var(--rot) + 5deg));
}
}
@keyframes cardRise {
0%, 100% {
transform: translateY(0);
box-shadow: 0 6px 0 rgba(36, 29, 46, 0.18);
}
50% {
transform: translateY(-10px);
box-shadow: 0 16px 0 rgba(36, 29, 46, 0.12);
}
}
@media (max-width: 360px), (max-height: 230px) {
.hcol {
width: 168px;
}
.hcol__card {
padding: 12px;
gap: 4px;
}
.hcol__desc {
display: none;
}
.hcol__title {
font-size: 18px;
}
.hcol__frame--coral {
width: 48px;
height: 60px;
left: -20px;
top: -10px;
}
.hcol__frame--sky {
width: 44px;
height: 52px;
right: -18px;
top: -14px;
}
.hcol__frame--lime {
width: 56px;
height: 36px;
right: -12px;
bottom: -16px;
}
.hcol__stamp {
width: 28px;
font-size: 14px;
right: -14px;
top: -16px;
}
}
@media (prefers-reduced-motion: reduce) {
.hcol__frame, .hcol__card {
animation: none !important;
}
.hcol__frame--coral {
transform: rotate(-9deg);
}
.hcol__frame--sky {
transform: rotate(8deg);
}
.hcol__frame--lime {
transform: rotate(-5deg);
}
}import type { CSSProperties } from "react";
import styles from "./HeroCollage.module.scss";
export type HeroCollageProps = {
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 HeroCollage({
eyebrow = "ISSUE 04",
title = "오려 붙인 편집",
desc = "겹치고 기울인 화면",
cta = "지금 보기",
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#d93a1e",
}: HeroCollageProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.hcol} style={vars}>
<span className={`${styles.frame} ${styles.frameCoral}`} aria-hidden="true" />
<span className={`${styles.frame} ${styles.frameSky}`} aria-hidden="true" />
<span className={`${styles.frame} ${styles.frameLime}`} aria-hidden="true" />
<div className={styles.card}>
<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>
<span className={styles.stamp} aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M40.2,95.8a28,28,0,1,1,39.6,0A28,28,0,0,1,40.2,95.8Zm0,64.4a28,28,0,1,0,39.6,0A28,28,0,0,0,40.2,160.2Z" opacity="0.2"/><path d="M157.73,113.13A8,8,0,0,1,159.82,102L227.48,55.7a8,8,0,0,1,9,13.21l-67.67,46.3a7.92,7.92,0,0,1-4.51,1.4A8,8,0,0,1,157.73,113.13Zm80.87,85.09a8,8,0,0,1-11.12,2.08L136,137.7,93.49,166.78a36,36,0,1,1-9-13.19L121.83,128,84.44,102.41a35.86,35.86,0,1,1,9-13.19l143,97.87A8,8,0,0,1,238.6,198.22ZM80,180a20,20,0,1,0-5.86,14.14A19.85,19.85,0,0,0,80,180ZM74.14,90.13a20,20,0,1,0-28.28,0A19.85,19.85,0,0,0,74.14,90.13Z"/></svg>
</span>
</section>
);
}01 · Stacked frame hero — Measured area 13.358% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where to look in the code
Move the front and the back the same way and the whole stack flattens into one plane.
.hcol__frame {
border: 6px solid #fff; /* the white margin does the photo-border job */
animation-name: frameSway;
animation-duration: 2s;
animation-iteration-count: infinite;
}
.hcol__frame--sky { --rot: 8deg; animation-delay: -.7s; }
.hcol__frame--lime { --rot: -5deg; animation-delay: -1.4s; }
@keyframes frameSway {
0%, 100% { transform: translateY(0) rotate(var(--rot)); }
50% { transform: translateY(8px) rotate(calc(var(--rot) + 5deg)); }
}
02Magazine index nav
What happens on screen
Put a two-digit page number above every menu item and the bar turns into a table of contents.

<main class="stage">
<nav class="ncol" aria-label="Contents">
<p class="ncol__mark">COLLAGE</p>
<div class="ncol__bar">
<span class="ncol__chip" aria-hidden="true"></span>
<ul class="ncol__tabs">
<li class="ncol__tab"><span class="ncol__no">01</span><span class="ncol__label">Cover</span></li>
<li class="ncol__tab"><span class="ncol__no">02</span><span class="ncol__label">Feature</span></li>
<li class="ncol__tab"><span class="ncol__no">03</span><span class="ncol__label">Photos</span></li>
<li class="ncol__tab"><span class="ncol__no">04</span><span class="ncol__label">Reviews</span></li>
</ul>
</div>
<p class="ncol__rule" aria-hidden="true"></p>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
// 콜라주·맥시멀 팔레트 (01 과 같은 값). 글자는 ink·slate 뿐, 레몬은 면적 전용
$cg-cream: #fbf3e4;
$cg-sheet: #ffffff;
$cg-ink: #241d2e;
$cg-slate: #57506a;
$cg-coral: #bb7f74;
$cg-sky: #668cb4;
$cg-lemon: #d1c8a3;
$serif: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
$duration: $dur-loop; // 칩이 네 칸을 왕복하는 한 주기
$easing: $ease-spring;
$color: $cg-lemon; // 현재 위치를 표시하는 종이 칩 색
@include stage($cg-cream);
.ncol {
position: relative;
width: clamp(236px, 82vw, 316px);
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-2;
}
.ncol__mark {
margin: 0;
color: $cg-ink;
font-family: $serif;
font-weight: 700;
font-size: clamp(18px, 4.6vw, 24px);
line-height: 1;
letter-spacing: .22em;
transform: rotate(-2deg);
}
.ncol__bar {
position: relative;
box-sizing: border-box;
padding: $sp-2;
border-radius: $r-xs;
background: $cg-sheet;
box-shadow: 0 5px 0 rgba($cg-ink, .16);
}
.ncol__tabs {
position: relative;
z-index: 1;
display: flex;
margin: 0;
padding: 0;
list-style: none;
}
// BD19 — 이동량이 고정 px 이므로 탭은 반드시 내용 폭이 아니라 고정 폭이어야 한다
.ncol__tab {
flex: 0 0 var(--tab);
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-1;
padding: $sp-1 0;
margin-right: $sp-1;
}
.ncol__tab:last-child { margin-right: 0; }
.ncol__no {
color: $cg-ink;
font-family: $serif;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 12px);
line-height: 1;
}
.ncol__label {
color: $cg-ink;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
// 종이 칩 — 탭 뒤에서 한 칸씩 뛴다. 칸 간격(--step) = 탭 폭 + 간격
.ncol__chip {
position: absolute;
z-index: 0;
left: $sp-2;
top: $sp-2;
width: var(--tab);
height: calc(100% - #{$sp-4});
border-radius: $r-xs;
background: $cg-lemon;
animation-name: chipHop;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.ncol__rule {
width: 100%;
height: 3px;
margin: 0;
border-radius: $r-xs;
background: repeating-linear-gradient(90deg, $cg-ink 0 12px, transparent 12px 20px);
transform: rotate(1deg);
}
.ncol { --tab: 68px; --step: 72px; }
// 데모: 칸마다 잠깐 앉았다가 다음 칸으로 뛴다. 네 칸을 갔다가 같은 길로 돌아오므로
// 끝에서 처음으로 건너뛰는 구간이 없다 — 마지막 프레임과 첫 프레임이 같은 자리다
@keyframes chipHop {
0%, 10% { transform: translateX(0) translateY(0) rotate(-3deg); }
13.3% { transform: translateX(calc(var(--step) * .5)) translateY(-11px) rotate(5deg); }
16.6%, 26.6% { transform: translateX(var(--step)) translateY(0) rotate(-3deg); }
30% { transform: translateX(calc(var(--step) * 1.5)) translateY(-11px) rotate(5deg); }
33.3%, 43.3% { transform: translateX(calc(var(--step) * 2)) translateY(0) rotate(-3deg); }
46.6% { transform: translateX(calc(var(--step) * 2.5)) translateY(-11px) rotate(5deg); }
50%, 60% { transform: translateX(calc(var(--step) * 3)) translateY(0) rotate(-3deg); }
63.3% { transform: translateX(calc(var(--step) * 2.5)) translateY(-11px) rotate(-11deg); }
66.6%, 76.6% { transform: translateX(calc(var(--step) * 2)) translateY(0) rotate(-3deg); }
80% { transform: translateX(calc(var(--step) * 1.5)) translateY(-11px) rotate(-11deg); }
83.3%, 93.3% { transform: translateX(var(--step)) translateY(0) rotate(-3deg); }
96.6% { transform: translateX(calc(var(--step) * .5)) translateY(-11px) rotate(-11deg); }
100% { transform: translateX(0) translateY(0) rotate(-3deg); }
}
@media (max-width: 360px), (max-height: 230px) {
.ncol { width: 232px; --tab: 48px; --step: 52px; gap: $sp-2; }
.ncol__mark { font-size: 15px; letter-spacing: .14em; }
.ncol__label { font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.ncol__chip { animation: none !important; transform: rotate(-3deg); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf3e4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ncol {
position: relative;
width: clamp(236px, 82vw, 316px);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.ncol__mark {
margin: 0;
color: #241d2e;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: clamp(18px, 4.6vw, 24px);
line-height: 1;
letter-spacing: 0.22em;
transform: rotate(-2deg);
}
.ncol__bar {
position: relative;
box-sizing: border-box;
padding: 8px;
border-radius: 4px;
background: #ffffff;
box-shadow: 0 5px 0 rgba(36, 29, 46, 0.16);
}
.ncol__tabs {
position: relative;
z-index: 1;
display: flex;
margin: 0;
padding: 0;
list-style: none;
}
.ncol__tab {
flex: 0 0 var(--tab);
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 4px 0;
margin-right: 4px;
}
.ncol__tab:last-child {
margin-right: 0;
}
.ncol__no {
color: #241d2e;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 12px);
line-height: 1;
}
.ncol__label {
color: #241d2e;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
.ncol__chip {
position: absolute;
z-index: 0;
left: 8px;
top: 8px;
width: var(--tab);
height: calc(100% - 16px);
border-radius: 4px;
background: #d1c8a3;
animation-name: chipHop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.ncol__rule {
width: 100%;
height: 3px;
margin: 0;
border-radius: 4px;
background: repeating-linear-gradient(90deg, #241d2e 0 12px, transparent 12px 20px);
transform: rotate(1deg);
}
.ncol {
--tab: 68px;
--step: 72px;
}
@keyframes chipHop {
0%, 10% {
transform: translateX(0) translateY(0) rotate(-3deg);
}
13.3% {
transform: translateX(calc(var(--step) * 0.5)) translateY(-11px) rotate(5deg);
}
16.6%, 26.6% {
transform: translateX(var(--step)) translateY(0) rotate(-3deg);
}
30% {
transform: translateX(calc(var(--step) * 1.5)) translateY(-11px) rotate(5deg);
}
33.3%, 43.3% {
transform: translateX(calc(var(--step) * 2)) translateY(0) rotate(-3deg);
}
46.6% {
transform: translateX(calc(var(--step) * 2.5)) translateY(-11px) rotate(5deg);
}
50%, 60% {
transform: translateX(calc(var(--step) * 3)) translateY(0) rotate(-3deg);
}
63.3% {
transform: translateX(calc(var(--step) * 2.5)) translateY(-11px) rotate(-11deg);
}
66.6%, 76.6% {
transform: translateX(calc(var(--step) * 2)) translateY(0) rotate(-3deg);
}
80% {
transform: translateX(calc(var(--step) * 1.5)) translateY(-11px) rotate(-11deg);
}
83.3%, 93.3% {
transform: translateX(var(--step)) translateY(0) rotate(-3deg);
}
96.6% {
transform: translateX(calc(var(--step) * 0.5)) translateY(-11px) rotate(-11deg);
}
100% {
transform: translateX(0) translateY(0) rotate(-3deg);
}
}
@media (max-width: 360px), (max-height: 230px) {
.ncol {
width: 232px;
--tab: 48px;
--step: 52px;
gap: 8px;
}
.ncol__mark {
font-size: 15px;
letter-spacing: 0.14em;
}
.ncol__label {
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.ncol__chip {
animation: none !important;
transform: rotate(-3deg);
}
}import type { CSSProperties } from "react";
import styles from "./NavCollage.module.scss";
export type NavCollageItem = { no: string; label: string };
export type NavCollageProps = {
mark?: string;
items?: NavCollageItem[];
/** 탭 한 칸의 고정 폭(px). 이동량이 고정 px 이라 탭도 고정 폭이어야 한다 */
tab?: number;
/** 탭 사이 간격(px) */
gap?: number;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string; "--easing"?: string; "--color"?: string;
"--tab"?: string; "--step"?: string;
};
const defaultItems: NavCollageItem[] = [
{ no: "01", label: "표지" },
{ no: "02", label: "특집" },
{ no: "03", label: "화보" },
{ no: "04", label: "후기" },
];
export default function NavCollage({
mark = "COLLAGE",
items = defaultItems,
tab = 68,
gap = 4,
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#ffd93d",
}: NavCollageProps) {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
"--tab": `${tab}px`,
"--step": `${tab + gap}px`,
};
return (
<nav className={styles.ncol} style={vars} aria-label={mark}>
<p className={styles.mark}>{mark}</p>
<div className={styles.bar}>
<span className={styles.chip} aria-hidden="true" />
<ul className={styles.tabs}>
{items.map((x) => (
<li key={x.no} className={styles.tab}>
<span className={styles.no}>{x.no}</span>
<span className={styles.label}>{x.label}</span>
</li>
))}
</ul>
</div>
<p className={styles.rule} aria-hidden="true" />
</nav>
);
}02 · Magazine index nav — Measured area 8.741% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password
How to read the code
The lemon chip sits on a tab for a beat before jumping to the next one — without that pause, nothing tells you which tab you're on.
.ncol { --tab: 68px; --step: 72px; } /* measured: 68px tab + 4px gap */
.ncol__tab { flex: 0 0 var(--tab); } /* equal-share flex would break the hop distance */
@keyframes chipHop {
0%, 10% { transform: translateX(0) rotate(-3deg); }
13.3% { transform: translateX(calc(var(--step) * .5)) translateY(-11px) rotate(5deg); }
16.6%, 26.6% { transform: translateX(var(--step)) rotate(-3deg); }
50%, 60% { transform: translateX(calc(var(--step) * 3)) rotate(-3deg); }
100% { transform: translateX(0) rotate(-3deg); }
}
03Cut-out frame card grid
What moves
Each card straightens from its pasted angle to zero and tilts back.

<main class="stage">
<section class="ccol">
<article class="ccol__card ccol__card--coral">
<span class="ccol__photo" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M208,64H176L160,40H96L80,64H48A16,16,0,0,0,32,80V192a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V80A16,16,0,0,0,208,64ZM128,168a36,36,0,1,1,36-36A36,36,0,0,1,128,168Z" opacity="0.2"/><path d="M208,56H180.28L166.65,35.56A8,8,0,0,0,160,32H96a8,8,0,0,0-6.65,3.56L75.71,56H48A24,24,0,0,0,24,80V192a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V80A24,24,0,0,0,208,56Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H80a8,8,0,0,0,6.66-3.56L100.28,48h55.43l13.63,20.44A8,8,0,0,0,176,72h32a8,8,0,0,1,8,8ZM128,88a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,88Zm0,72a28,28,0,1,1,28-28A28,28,0,0,1,128,160Z"/></svg>
</span>
<p class="ccol__no">01</p>
<p class="ccol__name">Photos</p>
</article>
<article class="ccol__card ccol__card--sky">
<span class="ccol__photo" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M224,127.17a96.48,96.48,0,0,1-2.39,22.18A24,24,0,0,1,198.21,168H152a24,24,0,0,0-24,24,24,24,0,0,1-32,22.61C58.73,201.44,32,169.81,32,128a96,96,0,0,1,95-96C179.84,31.47,223.55,74.35,224,127.17Z" opacity="0.2"/><path d="M200.77,53.89A103.27,103.27,0,0,0,128,24h-1.07A104,104,0,0,0,24,128c0,43,26.58,79.06,69.36,94.17A32,32,0,0,0,136,192a16,16,0,0,1,16-16h46.21a31.81,31.81,0,0,0,31.2-24.88,104.43,104.43,0,0,0,2.59-24A103.28,103.28,0,0,0,200.77,53.89Zm13,93.71A15.89,15.89,0,0,1,198.21,160H152a32,32,0,0,0-32,32,16,16,0,0,1-21.31,15.07C62.49,194.3,40,164,40,128a88,88,0,0,1,87.09-88h.9a88.35,88.35,0,0,1,88,87.25A88.86,88.86,0,0,1,213.81,147.6ZM140,76a12,12,0,1,1-12-12A12,12,0,0,1,140,76ZM96,100A12,12,0,1,1,84,88,12,12,0,0,1,96,100Zm0,56a12,12,0,1,1-12-12A12,12,0,0,1,96,156Zm88-56a12,12,0,1,1-12-12A12,12,0,0,1,184,100Z"/></svg>
</span>
<p class="ccol__no">02</p>
<p class="ccol__name">Color field</p>
</article>
<article class="ccol__card ccol__card--grape">
<span class="ccol__photo" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M128,48,68.32,70.38a8,8,0,0,0-5.08,6.17L40,216l139.45-23.24a8,8,0,0,0,6.17-5.08L208,128Zm-4,104a20,20,0,1,1,20-20A20,20,0,0,1,124,152Z" opacity="0.2"/><path d="M248,92.68a15.86,15.86,0,0,0-4.69-11.31L174.63,12.68a16,16,0,0,0-22.63,0L123.57,41.11l-58,21.77A16.06,16.06,0,0,0,55.35,75.23L32.11,214.68A8,8,0,0,0,40,224a8.4,8.4,0,0,0,1.32-.11l139.44-23.24a16,16,0,0,0,12.35-10.17l21.77-58L243.31,104A15.87,15.87,0,0,0,248,92.68Zm-69.87,92.19L63.32,204l47.37-47.37a28,28,0,1,0-11.32-11.32L52,192.7,71.13,77.86,126,57.29,198.7,130ZM112,132a12,12,0,1,1,12,12A12,12,0,0,1,112,132Zm96-15.32L139.31,48l24-24L232,92.68Z"/></svg>
</span>
<p class="ccol__no">03</p>
<p class="ccol__name">Type</p>
</article>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 콜라주·맥시멀 팔레트 (01 과 같은 값). 색면 위 글자는 흰색, 크림 위 글자는 ink·slate
$cg-cream: #fbf3e4;
$cg-sheet: #ffffff;
$cg-ink: #241d2e;
$cg-slate: #57506a;
$cg-coral: #bb7f74;
$cg-sky: #668cb4;
$cg-grape: #6b3fa0;
$cg-lemon: #d1c8a3;
$cg-lime: #bfce9c;
$serif: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
$duration: $dur-loop; // 카드 한 장이 펴졌다 다시 기우는 한 주기
$easing: $ease-spring;
$color: $cg-coral; // 첫 카드의 색면
@include stage($cg-cream);
.ccol {
display: flex;
align-items: flex-start;
gap: $sp-3;
}
// 오려 붙인 카드 — 흰 여백이 사진 테두리 노릇을 한다
.ccol__card {
box-sizing: border-box;
width: 84px;
padding: $sp-2 $sp-2 $sp-1;
border-radius: $r-xs;
background: $cg-sheet;
box-shadow: 0 5px 0 rgba($cg-ink, .16);
animation-name: cardStraighten;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.ccol__card--coral { --rot: -7deg; }
.ccol__card--sky { --rot: 5deg; animation-delay: -.24s; }
.ccol__card--grape { --rot: -4deg; animation-delay: -.48s; }
.ccol__photo {
display: grid;
place-items: center;
height: 56px;
border-radius: $r-xs;
color: $cg-sheet;
font-size: 24px;
}
.ccol__card--coral .ccol__photo { background: linear-gradient(150deg, $cg-coral 0%, $cg-lemon 100%); }
.ccol__card--sky .ccol__photo { background: linear-gradient(150deg, $cg-sky 0%, $cg-lime 100%); }
.ccol__card--grape .ccol__photo { background: linear-gradient(150deg, $cg-grape 0%, $cg-coral 100%); }
.ccol__no {
margin: $sp-2 0 0;
color: $cg-coral;
font-family: $serif;
font-weight: 700;
font-size: 12px;
line-height: 1;
}
.ccol__name {
margin: $sp-1 0 0;
color: $cg-ink;
font-family: $serif;
font-weight: 700;
font-size: 15px;
line-height: 1;
}
// 데모: 붙인 각도에서 0도까지 펴졌다 되돌아간다 — 세 장이 0.24초씩 어긋난다
@keyframes cardStraighten {
0%, 100% { transform: rotate(var(--rot)); box-shadow: 0 5px 0 rgba($cg-ink, .16); }
45% { transform: rotate(0deg) translateY(-8px); box-shadow: 0 13px 0 rgba($cg-ink, .12); }
}
@media (max-width: 360px), (max-height: 230px) {
.ccol { gap: $sp-2; }
.ccol__card { width: 64px; padding: $sp-1; }
.ccol__photo { height: 40px; font-size: 18px; }
.ccol__name { font-size: 13px; }
}
@media (prefers-reduced-motion: reduce) {
.ccol__card { animation: none !important; transform: rotate(var(--rot)); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf3e4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ccol {
display: flex;
align-items: flex-start;
gap: 12px;
}
.ccol__card {
box-sizing: border-box;
width: 84px;
padding: 8px 8px 4px;
border-radius: 4px;
background: #ffffff;
box-shadow: 0 5px 0 rgba(36, 29, 46, 0.16);
animation-name: cardStraighten;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.ccol__card--coral {
--rot: -7deg;
}
.ccol__card--sky {
--rot: 5deg;
animation-delay: -0.24s;
}
.ccol__card--grape {
--rot: -4deg;
animation-delay: -0.48s;
}
.ccol__photo {
display: grid;
place-items: center;
height: 56px;
border-radius: 4px;
color: #ffffff;
font-size: 24px;
}
.ccol__card--coral .ccol__photo {
background: linear-gradient(150deg, #bb7f74 0%, #d1c8a3 100%);
}
.ccol__card--sky .ccol__photo {
background: linear-gradient(150deg, #668cb4 0%, #bfce9c 100%);
}
.ccol__card--grape .ccol__photo {
background: linear-gradient(150deg, #6b3fa0 0%, #bb7f74 100%);
}
.ccol__no {
margin: 8px 0 0;
color: #bb7f74;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: 12px;
line-height: 1;
}
.ccol__name {
margin: 4px 0 0;
color: #241d2e;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: 15px;
line-height: 1;
}
@keyframes cardStraighten {
0%, 100% {
transform: rotate(var(--rot));
box-shadow: 0 5px 0 rgba(36, 29, 46, 0.16);
}
45% {
transform: rotate(0deg) translateY(-8px);
box-shadow: 0 13px 0 rgba(36, 29, 46, 0.12);
}
}
@media (max-width: 360px), (max-height: 230px) {
.ccol {
gap: 8px;
}
.ccol__card {
width: 64px;
padding: 4px;
}
.ccol__photo {
height: 40px;
font-size: 18px;
}
.ccol__name {
font-size: 13px;
}
}
@media (prefers-reduced-motion: reduce) {
.ccol__card {
animation: none !important;
transform: rotate(var(--rot));
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./CardGridCollage.module.scss";
export type CardGridCollageCard = { no: string; name: string; icon?: ReactNode };
export type CardGridCollageProps = {
cards?: CardGridCollageCard[];
/** 카드끼리 어긋나는 간격(ms) */
stagger?: number;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type CardVars = CSSProperties & { "--rot"?: string; animationDelay?: string };
const defaultCards: CardGridCollageCard[] = [
{ no: "01", name: "화보" },
{ no: "02", name: "색면" },
{ no: "03", name: "활자" },
];
const angles = ["-7deg", "5deg", "-4deg"];
const colorFills = [
"linear-gradient(150deg, #d93a1e 0%, #ffd93d 100%)",
"linear-gradient(150deg, #2d6fb5 0%, #b6e34f 100%)",
"linear-gradient(150deg, #6b3fa0 0%, #d93a1e 100%)",
];
export default function CardGridCollage({
cards = defaultCards,
stagger = 240,
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#d93a1e",
}: CardGridCollageProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.ccol} style={vars}>
{cards.map((c, i) => {
const cv: CardVars = { "--rot": angles[i % angles.length], animationDelay: `${-stagger * i}ms` };
return (
<article key={c.no} className={styles.card} style={cv}>
<span className={styles.photo} style={{ background: colorFills[i % colorFills.length] }} aria-hidden="true">
{c.icon}
</span>
<p className={styles.no}>{c.no}</p>
<p className={styles.name}>{c.name}</p>
</article>
);
})}
</section>
);
}03 · Cut-out frame card grid — Measured area 9.509% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
What to change
Offsetting them by 0.24s is what keeps the row reading as three separate clippings rather than one block.
.ccol__card--coral { --rot: -7deg; }
.ccol__card--sky { --rot: 5deg; animation-delay: -.24s; }
.ccol__card--grape { --rot: -4deg; animation-delay: -.48s; }
@keyframes cardStraighten {
0%, 100% { transform: rotate(var(--rot)); box-shadow: 0 5px 0 rgba(36, 29, 46, .16); }
45% { transform: rotate(0deg) translateY(-8px); box-shadow: 0 13px 0 rgba(36, 29, 46, .12); }
}
04Rotated stamp badges
What you see
One dashed border is enough to turn a circle into a rubber stamp.

<main class="stage">
<section class="scol">
<span class="scol__badge scol__badge--lemon">
<span class="scol__icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M229.06,108.79l-48.7,42,14.88,62.79a8.4,8.4,0,0,1-12.52,9.17L128,189.09,73.28,222.74a8.4,8.4,0,0,1-12.52-9.17l14.88-62.79-48.7-42A8.46,8.46,0,0,1,31.73,94L95.64,88.8l24.62-59.6a8.36,8.36,0,0,1,15.48,0l24.62,59.6L224.27,94A8.46,8.46,0,0,1,229.06,108.79Z" opacity="0.2"/><path d="M239.18,97.26A16.38,16.38,0,0,0,224.92,86l-59-4.76L143.14,26.15a16.36,16.36,0,0,0-30.27,0L90.11,81.23,31.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.82L128,198.49l50.53,31.08A16.4,16.4,0,0,0,203,211.75l-13.76-58.07,45-38.83A16.43,16.43,0,0,0,239.18,97.26Zm-15.34,5.47-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,0L69.09,215.94c-.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,103,91.86l24.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.29S224,102.63,223.84,102.73Z"/></svg>
</span>
<span class="scol__word">BEST</span>
</span>
<span class="scol__badge scol__badge--lime">
<span class="scol__icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M96,240l16-80L48,136,160,16,144,96l64,24Z" opacity="0.2"/><path d="M215.79,118.17a8,8,0,0,0-5-5.66L153.18,90.9l14.66-73.33a8,8,0,0,0-13.69-7l-112,120a8,8,0,0,0,3,13l57.63,21.61L88.16,238.43a8,8,0,0,0,13.69,7l112-120A8,8,0,0,0,215.79,118.17ZM109.37,214l10.47-52.38a8,8,0,0,0-5-9.06L62,132.71l84.62-90.66L136.16,94.43a8,8,0,0,0,5,9.06l52.8,19.8Z"/></svg>
</span>
<span class="scol__word">NEW</span>
</span>
<span class="scol__badge scol__badge--blush">
<span class="scol__icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M232,102c0,66-104,122-104,122S24,168,24,102A54,54,0,0,1,78,48c22.59,0,41.94,12.31,50,32,8.06-19.69,27.41-32,50-32A54,54,0,0,1,232,102Z" opacity="0.2"/><path d="M178,40c-20.65,0-38.73,8.88-50,23.89C116.73,48.88,98.65,40,78,40a62.07,62.07,0,0,0-62,62c0,70,103.79,126.66,108.21,129a8,8,0,0,0,7.58,0C136.21,228.66,240,172,240,102A62.07,62.07,0,0,0,178,40ZM128,214.8C109.74,204.16,32,155.69,32,102A46.06,46.06,0,0,1,78,56c19.45,0,35.78,10.36,42.6,27a8,8,0,0,0,14.8,0c6.82-16.67,23.15-27,42.6-27a46.06,46.06,0,0,1,46,46C224,155.61,146.24,204.15,128,214.8Z"/></svg>
</span>
<span class="scol__word">PICK</span>
</span>
<span class="scol__badge scol__badge--sheet">
<span class="scol__icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z" opacity="0.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,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"/></svg>
</span>
<span class="scol__word">2026</span>
</span>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 콜라주·맥시멀 팔레트 (01 과 같은 값). 레몬·라임·블러시는 밝아서 ink 글자만 얹는다
$cg-cream: #fbf3e4;
$cg-sheet: #ffffff;
$cg-ink: #241d2e;
$cg-coral: #bb7f74;
$cg-lemon: #d1c8a3;
$cg-lime: #bfce9c;
$cg-blush: #ffb3c7;
$serif: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
$duration: $dur-loop; // 배지 네 개가 한 바퀴 다 찍히는 한 주기
$easing: $ease-pop; // 도장은 끝에서 튄다
$color: $cg-lemon; // 첫 배지의 면 색
@include stage($cg-cream);
.scol {
display: flex;
align-items: center;
gap: $sp-3;
}
// 도장 배지 — 점선 테두리가 스탬프 가장자리, 기울기는 배지마다 고정
.scol__badge {
box-sizing: border-box;
width: 66px;
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: $sp-1;
border: 2px dashed rgba($cg-ink, .55);
border-radius: 50%;
color: $cg-ink;
box-shadow: 0 4px 0 rgba($cg-ink, .16);
animation-name: stampPress;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.scol__badge--lemon { --rot: -12deg; background: $cg-lemon; }
.scol__badge--lime { --rot: 8deg; background: $cg-lime; animation-delay: -.5s; }
.scol__badge--blush { --rot: -6deg; background: $cg-blush; animation-delay: -1s; }
.scol__badge--sheet { --rot: 10deg; background: $cg-sheet; animation-delay: -1.5s; }
.scol__icon { font-size: 22px; line-height: 1; }
.scol__word {
font-family: $serif;
font-weight: 700;
font-size: 11px;
line-height: 1;
letter-spacing: .1em;
}
// 데모: 눌렸다(0.55) 튀어 오른(1.16) 뒤 제자리. 기울기는 keyframe 마다 다시 써야 유지된다
@keyframes stampPress {
0%, 55%, 100% { transform: rotate(var(--rot)) scale(1); }
8% { transform: rotate(var(--rot)) scale(.55); }
20% { transform: rotate(var(--rot)) scale(1.16); }
32% { transform: rotate(var(--rot)) scale(1); }
}
@media (max-width: 360px), (max-height: 230px) {
.scol { gap: $sp-2; }
.scol__badge { width: 52px; }
.scol__icon { font-size: 16px; }
.scol__word { font-size: 9px; }
}
@media (prefers-reduced-motion: reduce) {
.scol__badge { animation: none !important; transform: rotate(var(--rot)); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf3e4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.scol {
display: flex;
align-items: center;
gap: 12px;
}
.scol__badge {
box-sizing: border-box;
width: 66px;
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
border: 2px dashed rgba(36, 29, 46, 0.55);
border-radius: 50%;
color: #241d2e;
box-shadow: 0 4px 0 rgba(36, 29, 46, 0.16);
animation-name: stampPress;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
}
.scol__badge--lemon {
--rot: -12deg;
background: #d1c8a3;
}
.scol__badge--lime {
--rot: 8deg;
background: #bfce9c;
animation-delay: -0.5s;
}
.scol__badge--blush {
--rot: -6deg;
background: #ffb3c7;
animation-delay: -1s;
}
.scol__badge--sheet {
--rot: 10deg;
background: #ffffff;
animation-delay: -1.5s;
}
.scol__icon {
font-size: 22px;
line-height: 1;
}
.scol__word {
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: 11px;
line-height: 1;
letter-spacing: 0.1em;
}
@keyframes stampPress {
0%, 55%, 100% {
transform: rotate(var(--rot)) scale(1);
}
8% {
transform: rotate(var(--rot)) scale(0.55);
}
20% {
transform: rotate(var(--rot)) scale(1.16);
}
32% {
transform: rotate(var(--rot)) scale(1);
}
}
@media (max-width: 360px), (max-height: 230px) {
.scol {
gap: 8px;
}
.scol__badge {
width: 52px;
}
.scol__icon {
font-size: 16px;
}
.scol__word {
font-size: 9px;
}
}
@media (prefers-reduced-motion: reduce) {
.scol__badge {
animation: none !important;
transform: rotate(var(--rot));
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./StickerBadgesCollage.module.scss";
export type StickerBadge = { word: string; bg?: string; rot?: string; icon?: ReactNode };
export type StickerBadgesCollageProps = {
badges?: StickerBadge[];
/** 배지끼리 어긋나는 간격(ms) */
stagger?: number;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type BadgeVars = CSSProperties & { "--rot"?: string; background?: string; animationDelay?: string };
const defaultBadges: StickerBadge[] = [
{ word: "BEST", bg: "#ffd93d", rot: "-12deg" },
{ word: "NEW", bg: "#b6e34f", rot: "8deg" },
{ word: "PICK", bg: "#ffb3c7", rot: "-6deg" },
{ word: "2026", bg: "#ffffff", rot: "10deg" },
];
export default function StickerBadgesCollage({
badges = defaultBadges,
stagger = 500,
duration = 2000,
easing = "cubic-bezier(.2, 1.4, .4, 1)",
color = "#ffd93d",
}: StickerBadgesCollageProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.scol} style={vars}>
{badges.map((b, i) => {
const bv: BadgeVars = {
"--rot": b.rot ?? "0deg",
background: b.bg ?? "#fff",
animationDelay: `${-stagger * i}ms`,
};
return (
<span key={b.word} className={styles.badge} style={bv}>
<span className={styles.icon} aria-hidden="true">{b.icon}</span>
<span className={styles.word}>{b.word}</span>
</span>
);
})}
</section>
);
}04 · Rotated stamp badges — Measured area 13.465% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where to look in the code
Each badge squashes to 0.55 and springs back to 1.16, and the tilt survives only because rotate is repeated inside every transform value.
.scol__badge--lemon { --rot: -12deg; background: #ffd93d; }
.scol__badge--lime { --rot: 8deg; animation-delay: -.5s; }
@keyframes stampPress {
0%, 55%, 100% { transform: rotate(var(--rot)) scale(1); }
8% { transform: rotate(var(--rot)) scale(.55); }
20% { transform: rotate(var(--rot)) scale(1.16); }
32% { transform: rotate(var(--rot)) scale(1); }
}
05Marquee text band
What happens on screen
Duplicate the word set, slide exactly one set's width, and the end of a lap is pixel-identical to the first frame.

<main class="stage">
<section class="mcol">
<div class="mcol__band mcol__band--coral">
<div class="mcol__track">
<span class="mcol__bit">COLLAGE</span>
<span class="mcol__bit">MAXIMAL</span>
<span class="mcol__bit">PASTE</span>
<span class="mcol__bit">CUT</span>
<span class="mcol__bit" aria-hidden="true">COLLAGE</span>
<span class="mcol__bit" aria-hidden="true">MAXIMAL</span>
<span class="mcol__bit" aria-hidden="true">PASTE</span>
<span class="mcol__bit" aria-hidden="true">CUT</span>
</div>
</div>
<div class="mcol__band mcol__band--lemon">
<div class="mcol__track mcol__track--back">
<span class="mcol__bit">Overlap</span>
<span class="mcol__bit">Tilt</span>
<span class="mcol__bit">Cut out</span>
<span class="mcol__bit" aria-hidden="true">Overlap</span>
<span class="mcol__bit" aria-hidden="true">Tilt</span>
<span class="mcol__bit" aria-hidden="true">Cut out</span>
</div>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 콜라주·맥시멀 팔레트 (01 과 같은 값). 코랄 위는 흰 글자(4.59:1), 레몬 위는 ink 글자(11.82:1)
$cg-cream: #fbf3e4;
$cg-sheet: #ffffff;
$cg-ink: #241d2e;
$cg-coral: #bb7f74;
$cg-lemon: #d1c8a3;
$serif: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
$duration: $dur-loop; // 한 벌 폭을 다 흘려보내는 한 주기
$easing: linear; // 띠는 등속이어야 이음매가 안 보인다
$color: $cg-coral; // 위 띠의 바탕색
@include stage($cg-cream);
.mcol {
width: clamp(240px, 82vw, 340px);
display: flex;
flex-direction: column;
}
.mcol__band {
box-sizing: border-box;
overflow: hidden;
padding: $sp-2 0;
border-radius: $r-xs;
box-shadow: 0 4px 0 rgba($cg-ink, .18);
}
.mcol__band--coral {
z-index: 1;
background: $cg-coral;
color: $cg-sheet;
transform: rotate(-2.5deg);
}
// 아래 띠는 조금 좁게, 오른쪽으로 밀어 붙인다. 겹치는 건 띠 가장자리끼리만 — 글자를 덮으면 콜라주가 아니라 사고다
.mcol__band--lemon {
z-index: 2;
width: 88%;
margin-top: $sp-2;
margin-left: auto;
background: $cg-lemon;
color: $cg-ink;
transform: rotate(2deg);
}
// 트랙 — 같은 낱말 묶음을 두 벌 이어 붙이고 한 벌 폭(=50%)만 민다
.mcol__track {
display: flex;
width: max-content;
animation-name: stripFlow;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.mcol__track--back { animation-name: stripFlowBack; }
// 간격은 gap 이 아니라 낱개 margin-right — gap 은 2N 조각 사이에 2N-1 개뿐이라 -50% 가 어긋난다
.mcol__bit {
margin-right: $sp-6;
font-family: $serif;
font-weight: 700;
font-size: 17px;
line-height: 1.2;
letter-spacing: .06em;
white-space: nowrap;
}
@keyframes stripFlow {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
@keyframes stripFlowBack {
from { transform: translateX(-50%); }
to { transform: translateX(0); }
}
@media (max-width: 360px), (max-height: 230px) {
.mcol { width: 236px; }
.mcol__bit { font-size: 13px; margin-right: $sp-4; }
}
@media (prefers-reduced-motion: reduce) {
.mcol__track { animation: none !important; transform: translateX(0); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf3e4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.mcol {
width: clamp(240px, 82vw, 340px);
display: flex;
flex-direction: column;
}
.mcol__band {
box-sizing: border-box;
overflow: hidden;
padding: 8px 0;
border-radius: 4px;
box-shadow: 0 4px 0 rgba(36, 29, 46, 0.18);
}
.mcol__band--coral {
z-index: 1;
background: #bb7f74;
color: #ffffff;
transform: rotate(-2.5deg);
}
.mcol__band--lemon {
z-index: 2;
width: 88%;
margin-top: 8px;
margin-left: auto;
background: #d1c8a3;
color: #241d2e;
transform: rotate(2deg);
}
.mcol__track {
display: flex;
width: max-content;
animation-name: stripFlow;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.mcol__track--back {
animation-name: stripFlowBack;
}
.mcol__bit {
margin-right: 24px;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: 17px;
line-height: 1.2;
letter-spacing: 0.06em;
white-space: nowrap;
}
@keyframes stripFlow {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
@keyframes stripFlowBack {
from {
transform: translateX(-50%);
}
to {
transform: translateX(0);
}
}
@media (max-width: 360px), (max-height: 230px) {
.mcol {
width: 236px;
}
.mcol__bit {
font-size: 13px;
margin-right: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.mcol__track {
animation: none !important;
transform: translateX(0);
}
}import type { CSSProperties } from "react";
import styles from "./MarqueeBandCollage.module.scss";
export type MarqueeBandCollageProps = {
/** 위 띠의 낱말 한 벌. 화면에는 두 벌이 깔린다 */
top?: string[];
/** 아래 띠의 낱말 한 벌 */
bottom?: string[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const defaultTop = ["COLLAGE", "MAXIMAL", "PASTE", "CUT"];
const defaultBottom = ["겹치기", "기울이기", "오리기"];
export default function MarqueeBandCollage({
top = defaultTop,
bottom = defaultBottom,
duration = 2000,
easing = "linear",
color = "#d93a1e",
}: MarqueeBandCollageProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
// 한 벌을 그대로 복제해 두 벌을 깐다. 복제본은 스크린리더에서 숨긴다
const doubled = (xs: string[]) => [...xs, ...xs];
return (
<section className={styles.mcol} style={vars}>
<div className={`${styles.band} ${styles.bandCoral}`}>
<div className={styles.track}>
{doubled(top).map((w, i) => (
<span key={`t${i}`} className={styles.bit} aria-hidden={i >= top.length}>{w}</span>
))}
</div>
</div>
<div className={`${styles.band} ${styles.bandLemon}`}>
<div className={`${styles.track} ${styles.trackBack}`}>
{doubled(bottom).map((w, i) => (
<span key={`b${i}`} className={styles.bit} aria-hidden={i >= bottom.length}>{w}</span>
))}
</div>
</div>
</section>
);
}05 · Marquee text band — Measured area 6.316% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
How to read the code
Running the two bands in opposite directions makes them read as pushing against each other.
.mcol__track {
display: flex;
width: max-content;
animation-name: stripFlow;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.mcol__track--back { animation-name: stripFlowBack; }
.mcol__bit { margin-right: 24px; } /* a flex gap would throw the -50% off */
@keyframes stripFlow { from { transform: translateX(0); } to { transform: translateX(-50%); } }
06Hand-drawn underline quote
What moves
Send stroke-dashoffset from positive to zero and on into negative, and a single SVG path draws from the left, then erases from the left.

<main class="stage">
<figure class="tcol">
<span class="tcol__tape" aria-hidden="true"></span>
<span class="tcol__quote" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M108,72v72H40a8,8,0,0,1-8-8V72a8,8,0,0,1,8-8h60A8,8,0,0,1,108,72Zm108-8H156a8,8,0,0,0-8,8v64a8,8,0,0,0,8,8h68V72A8,8,0,0,0,216,64Z" opacity="0.2"/><path d="M100,56H40A16,16,0,0,0,24,72v64a16,16,0,0,0,16,16h60v8a32,32,0,0,1-32,32,8,8,0,0,0,0,16,48.05,48.05,0,0,0,48-48V72A16,16,0,0,0,100,56Zm0,80H40V72h60ZM216,56H156a16,16,0,0,0-16,16v64a16,16,0,0,0,16,16h60v8a32,32,0,0,1-32,32,8,8,0,0,0,0,16,48.05,48.05,0,0,0,48-48V72A16,16,0,0,0,216,56Zm0,80H156V72h60Z"/></svg>
</span>
<blockquote class="tcol__text">
A cut-and-paste screen, yet<br>
<span class="tcol__mark">the order never wobbles</span>
<svg class="tcol__line" viewBox="0 0 200 12" aria-hidden="true" preserveAspectRatio="none">
<path d="M3 8 C 28 2, 52 11, 78 6 S 132 2, 158 8 S 186 4, 197 6" />
</svg>
</blockquote>
<figcaption class="tcol__by">
<span class="tcol__dot" aria-hidden="true">J</span>
<span class="tcol__who">Jung · magazine editor</span>
</figcaption>
</figure>
</main>@use "../../../../site/static/demo-tokens" as *;
// 콜라주·맥시멀 팔레트 (01 과 같은 값)
$cg-cream: #fbf3e4;
$cg-sheet: #ffffff;
$cg-ink: #241d2e;
$cg-slate: #57506a;
$cg-coral: #bb7f74;
$cg-lemon: #d1c8a3;
$cg-lime: #bfce9c;
$serif: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
$duration: $dur-loop; // 선이 다 그어지고 지워지는 한 주기
$easing: $ease-out; // 손으로 긋는 선은 끝에서 느려진다
$color: $cg-coral; // 밑줄 색
$line-len: 230; // path 실측 길이보다 넉넉히 — dasharray·dashoffset 에 같이 쓴다
@include stage($cg-cream);
.tcol {
position: relative;
box-sizing: border-box;
width: clamp(216px, 74vw, 276px);
margin: 0;
padding: $sp-5 $sp-5 $sp-4;
border-radius: $r-xs;
background: $cg-sheet;
box-shadow: 0 6px 0 rgba($cg-ink, .16);
transform: rotate(-1.5deg);
}
// 비뚤게 붙인 마스킹 테이프 한 장
.tcol__tape {
position: absolute;
top: -10px;
left: 50%;
width: 72px;
height: 20px;
margin-left: -36px;
border-radius: 0;
background: rgba($cg-lime, .85);
transform: rotate(-5deg);
}
.tcol__quote {
display: block;
color: $cg-coral;
font-size: 20px;
line-height: 1;
}
.tcol__text {
position: relative;
margin: $sp-2 0 0;
color: $cg-ink;
font-family: $serif;
font-weight: 700;
font-size: clamp(14px, 3.4vw, 17px);
line-height: 1.5;
}
.tcol__mark { position: relative; z-index: 1; }
// 손으로 그은 것 같은 밑줄 — 글자 줄 아래에 딱 붙는다
.tcol__line {
position: absolute;
left: 0;
bottom: -4px;
width: 100%;
height: 12px;
overflow: visible;
fill: none;
stroke: $cg-coral;
stroke-width: 4;
stroke-linecap: round;
stroke-dasharray: $line-len;
animation-name: inkDraw;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.tcol__by {
display: flex;
align-items: center;
gap: $sp-2;
margin-top: $sp-4;
}
.tcol__dot {
width: 24px;
aspect-ratio: 1;
display: grid;
place-items: center;
border-radius: 50%;
background: $cg-lemon;
color: $cg-ink;
font-weight: 700;
font-size: 12px;
line-height: 1;
transform: rotate(6deg);
}
.tcol__who {
color: $cg-slate;
font-weight: 600;
font-size: 12px;
line-height: 1;
}
// 데모: 왼쪽부터 그어졌다가(오프셋 +→0) 왼쪽부터 지워진다(0→-) — 되돌아가는 구간이 없다
@keyframes inkDraw {
0% { stroke-dashoffset: #{$line-len}; }
45% { stroke-dashoffset: 0; }
70% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -#{$line-len}; }
}
@media (max-width: 360px), (max-height: 230px) {
.tcol { width: 208px; padding: $sp-4 $sp-3 $sp-3; }
.tcol__quote { font-size: 15px; }
.tcol__text { font-size: 13px; }
.tcol__by { margin-top: $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.tcol__line { animation: none !important; stroke-dashoffset: 0; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf3e4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.tcol {
position: relative;
box-sizing: border-box;
width: clamp(216px, 74vw, 276px);
margin: 0;
padding: 20px 20px 16px;
border-radius: 4px;
background: #ffffff;
box-shadow: 0 6px 0 rgba(36, 29, 46, 0.16);
transform: rotate(-1.5deg);
}
.tcol__tape {
position: absolute;
top: -10px;
left: 50%;
width: 72px;
height: 20px;
margin-left: -36px;
border-radius: 0;
background: rgba(191, 206, 156, 0.85);
transform: rotate(-5deg);
}
.tcol__quote {
display: block;
color: #bb7f74;
font-size: 20px;
line-height: 1;
}
.tcol__text {
position: relative;
margin: 8px 0 0;
color: #241d2e;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: clamp(14px, 3.4vw, 17px);
line-height: 1.5;
}
.tcol__mark {
position: relative;
z-index: 1;
}
.tcol__line {
position: absolute;
left: 0;
bottom: -4px;
width: 100%;
height: 12px;
overflow: visible;
fill: none;
stroke: #bb7f74;
stroke-width: 4;
stroke-linecap: round;
stroke-dasharray: 230;
animation-name: inkDraw;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.tcol__by {
display: flex;
align-items: center;
gap: 8px;
margin-top: 16px;
}
.tcol__dot {
width: 24px;
aspect-ratio: 1;
display: grid;
place-items: center;
border-radius: 50%;
background: #d1c8a3;
color: #241d2e;
font-weight: 700;
font-size: 12px;
line-height: 1;
transform: rotate(6deg);
}
.tcol__who {
color: #57506a;
font-weight: 600;
font-size: 12px;
line-height: 1;
}
@keyframes inkDraw {
0% {
stroke-dashoffset: 230;
}
45% {
stroke-dashoffset: 0;
}
70% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -230;
}
}
@media (max-width: 360px), (max-height: 230px) {
.tcol {
width: 208px;
padding: 16px 12px 12px;
}
.tcol__quote {
font-size: 15px;
}
.tcol__text {
font-size: 13px;
}
.tcol__by {
margin-top: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.tcol__line {
animation: none !important;
stroke-dashoffset: 0;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./TestimonialCollage.module.scss";
export type TestimonialCollageProps = {
lead?: string;
/** 밑줄이 그어지는 구절 */
marked?: string;
initial?: string;
who?: string;
quoteIcon?: ReactNode;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TestimonialCollage({
lead = "잘라 붙인 화면인데",
marked = "읽는 순서는 안 흔들려요",
initial = "ㅈ",
who = "정 · 잡지 에디터",
quoteIcon,
duration = 2000,
easing = "ease-out",
color = "#d93a1e",
}: TestimonialCollageProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<figure className={styles.tcol} style={vars}>
<span className={styles.tape} aria-hidden="true" />
<span className={styles.quote} aria-hidden="true">{quoteIcon}</span>
<blockquote className={styles.text}>
{lead}<br />
<span className={styles.mark}>{marked}</span>
<svg className={styles.line} viewBox="0 0 200 12" aria-hidden="true" preserveAspectRatio="none">
<path d="M3 8 C 28 2, 52 11, 78 6 S 132 2, 158 8 S 186 4, 197 6" />
</svg>
</blockquote>
<figcaption className={styles.by}>
<span className={styles.dot} aria-hidden="true">{initial}</span>
<span className={styles.who}>{who}</span>
</figcaption>
</figure>
);
}06 · Hand-drawn underline quote — Measured area 0.803% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password
What to change
Nothing ever runs backward, so the loop has no visible restart.
.tcol__line {
fill: none;
stroke: #d93a1e;
stroke-width: 4;
stroke-linecap: round;
stroke-dasharray: 230; /* comfortably longer than the measured path */
animation-name: inkDraw;
animation-duration: 2s;
animation-iteration-count: infinite;
}
@keyframes inkDraw {
0% { stroke-dashoffset: 230; }
45% { stroke-dashoffset: 0; }
70% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -230; }
}
07Overlapping pricing layers
What you see
A negative margin overlaps the two cards and z-index settles the order.

<main class="stage">
<section class="pcol">
<article class="pcol__plan pcol__plan--base">
<p class="pcol__tier">BASIC</p>
<p class="pcol__price">₩0</p>
<p class="pcol__note">3 parts</p>
</article>
<article class="pcol__plan pcol__plan--pick">
<span class="pcol__stamp" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M232,128c0,12.51-17.82,21.95-22.68,33.69-4.68,11.32,1.42,30.65-7.78,39.85s-28.53,3.1-39.85,7.78C150,214.18,140.5,232,128,232s-22-17.82-33.69-22.68c-11.32-4.68-30.65,1.42-39.85-7.78s-3.1-28.53-7.78-39.85C41.82,150,24,140.5,24,128s17.82-22,22.68-33.69C51.36,83,45.26,63.66,54.46,54.46S83,51.36,94.31,46.68C106.05,41.82,115.5,24,128,24S150,41.82,161.69,46.68c11.32,4.68,30.65-1.42,39.85,7.78s3.1,28.53,7.78,39.85C214.18,106.05,232,115.5,232,128Z" opacity="0.2"/><path d="M225.86,102.82c-3.77-3.94-7.67-8-9.14-11.57-1.36-3.27-1.44-8.69-1.52-13.94-.15-9.76-.31-20.82-8-28.51s-18.75-7.85-28.51-8c-5.25-.08-10.67-.16-13.94-1.52-3.56-1.47-7.63-5.37-11.57-9.14C146.28,23.51,138.44,16,128,16s-18.27,7.51-25.18,14.14c-3.94,3.77-8,7.67-11.57,9.14C88,40.64,82.56,40.72,77.31,40.8c-9.76.15-20.82.31-28.51,8S41,67.55,40.8,77.31c-.08,5.25-.16,10.67-1.52,13.94-1.47,3.56-5.37,7.63-9.14,11.57C23.51,109.72,16,117.56,16,128s7.51,18.27,14.14,25.18c3.77,3.94,7.67,8,9.14,11.57,1.36,3.27,1.44,8.69,1.52,13.94.15,9.76.31,20.82,8,28.51s18.75,7.85,28.51,8c5.25.08,10.67.16,13.94,1.52,3.56,1.47,7.63,5.37,11.57,9.14C109.72,232.49,117.56,240,128,240s18.27-7.51,25.18-14.14c3.94-3.77,8-7.67,11.57-9.14,3.27-1.36,8.69-1.44,13.94-1.52,9.76-.15,20.82-.31,28.51-8s7.85-18.75,8-28.51c.08-5.25.16-10.67,1.52-13.94,1.47-3.56,5.37-7.63,9.14-11.57C232.49,146.28,240,138.44,240,128S232.49,109.73,225.86,102.82Zm-11.55,39.29c-4.79,5-9.75,10.17-12.38,16.52-2.52,6.1-2.63,13.07-2.73,19.82-.1,7-.21,14.33-3.32,17.43s-10.39,3.22-17.43,3.32c-6.75.1-13.72.21-19.82,2.73-6.35,2.63-11.52,7.59-16.52,12.38S132,224,128,224s-9.15-4.92-14.11-9.69-10.17-9.75-16.52-12.38c-6.1-2.52-13.07-2.63-19.82-2.73-7-.1-14.33-.21-17.43-3.32s-3.22-10.39-3.32-17.43c-.1-6.75-.21-13.72-2.73-19.82-2.63-6.35-7.59-11.52-12.38-16.52S32,132,32,128s4.92-9.15,9.69-14.11,9.75-10.17,12.38-16.52c2.52-6.1,2.63-13.07,2.73-19.82.1-7,.21-14.33,3.32-17.43S70.51,56.9,77.55,56.8c6.75-.1,13.72-.21,19.82-2.73,6.35-2.63,11.52-7.59,16.52-12.38S124,32,128,32s9.15,4.92,14.11,9.69,10.17,9.75,16.52,12.38c6.1,2.52,13.07,2.63,19.82,2.73,7,.1,14.33.21,17.43,3.32s3.22,10.39,3.32,17.43c.1,6.75.21,13.72,2.73,19.82,2.63,6.35,7.59,11.52,12.38,16.52S224,124,224,128,219.08,137.15,214.31,142.11ZM173.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,173.66,98.34Z"/></svg>
</span>
<p class="pcol__tier">STUDIO</p>
<p class="pcol__price">₩9</p>
<p class="pcol__note">9 parts + zip</p>
</article>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 콜라주·맥시멀 팔레트 (01 과 같은 값). 코랄 면 위 글자만 흰색(4.59:1)
$cg-cream: #fbf3e4;
$cg-sheet: #ffffff;
$cg-ink: #241d2e;
$cg-slate: #57506a;
$cg-coral: #bb7f74;
$cg-lemon: #d1c8a3;
$serif: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
$duration: $dur-loop; // 추천 카드가 떴다 내려앉는 한 주기
$easing: $ease-spring;
$color: $cg-coral; // 추천 카드의 면 색
@include stage($cg-cream);
.pcol {
display: flex;
align-items: flex-end;
isolation: isolate;
}
.pcol__plan {
position: relative;
box-sizing: border-box;
width: 116px;
padding: $sp-4 $sp-3;
border-radius: $r-xs;
box-shadow: 0 5px 0 rgba($cg-ink, .16);
}
// 뒤 카드 — 앞 카드에 모서리를 덮인다
.pcol__plan--base {
z-index: 1;
background: $cg-sheet;
color: $cg-ink;
transform: rotate(-3deg);
}
// 앞 카드 — 음수 margin 으로 겹치고 z-index 로 위에 올라간다
.pcol__plan--pick {
z-index: 2;
margin-left: -$sp-5;
margin-bottom: $sp-4;
background: $cg-coral;
color: $cg-sheet;
animation-name: planLift;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.pcol__stamp {
position: absolute;
z-index: 3;
top: -14px;
right: -12px;
width: 34px;
aspect-ratio: 1;
display: grid;
place-items: center;
border-radius: 50%;
background: $cg-lemon;
color: $cg-ink;
font-size: 20px;
transform: rotate(16deg);
box-shadow: 0 3px 0 rgba($cg-ink, .2);
}
.pcol__tier {
margin: 0;
font-weight: 700;
font-size: 11px;
line-height: 1;
letter-spacing: .16em;
}
.pcol__plan--base .pcol__tier { color: $cg-coral; }
.pcol__price {
margin: $sp-2 0 0;
font-family: $serif;
font-weight: 700;
font-size: 30px;
line-height: 1;
}
.pcol__note {
margin: $sp-2 0 0;
font-weight: 500;
font-size: 11px;
line-height: 1.3;
}
.pcol__plan--base .pcol__note { color: $cg-slate; }
// 데모: 앞 카드가 뜨는 동안 그림자 두께가 같이 늘어난다 — 겹친 층이 층으로 읽힌다
@keyframes planLift {
0%, 100% { transform: rotate(2deg) translateY(0); box-shadow: 0 5px 0 rgba($cg-ink, .16); }
50% { transform: rotate(2deg) translateY(-12px); box-shadow: 0 17px 0 rgba($cg-ink, .12); }
}
@media (max-width: 360px), (max-height: 230px) {
.pcol__plan { width: 92px; padding: $sp-3 $sp-2; }
.pcol__price { font-size: 22px; }
.pcol__note { font-size: 10px; }
.pcol__stamp { width: 26px; font-size: 15px; top: -10px; right: -8px; }
}
@media (prefers-reduced-motion: reduce) {
.pcol__plan--pick { animation: none !important; transform: rotate(2deg); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf3e4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.pcol {
display: flex;
align-items: flex-end;
isolation: isolate;
}
.pcol__plan {
position: relative;
box-sizing: border-box;
width: 116px;
padding: 16px 12px;
border-radius: 4px;
box-shadow: 0 5px 0 rgba(36, 29, 46, 0.16);
}
.pcol__plan--base {
z-index: 1;
background: #ffffff;
color: #241d2e;
transform: rotate(-3deg);
}
.pcol__plan--pick {
z-index: 2;
margin-left: -20px;
margin-bottom: 16px;
background: #bb7f74;
color: #ffffff;
animation-name: planLift;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.pcol__stamp {
position: absolute;
z-index: 3;
top: -14px;
right: -12px;
width: 34px;
aspect-ratio: 1;
display: grid;
place-items: center;
border-radius: 50%;
background: #d1c8a3;
color: #241d2e;
font-size: 20px;
transform: rotate(16deg);
box-shadow: 0 3px 0 rgba(36, 29, 46, 0.2);
}
.pcol__tier {
margin: 0;
font-weight: 700;
font-size: 11px;
line-height: 1;
letter-spacing: 0.16em;
}
.pcol__plan--base .pcol__tier {
color: #bb7f74;
}
.pcol__price {
margin: 8px 0 0;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: 30px;
line-height: 1;
}
.pcol__note {
margin: 8px 0 0;
font-weight: 500;
font-size: 11px;
line-height: 1.3;
}
.pcol__plan--base .pcol__note {
color: #57506a;
}
@keyframes planLift {
0%, 100% {
transform: rotate(2deg) translateY(0);
box-shadow: 0 5px 0 rgba(36, 29, 46, 0.16);
}
50% {
transform: rotate(2deg) translateY(-12px);
box-shadow: 0 17px 0 rgba(36, 29, 46, 0.12);
}
}
@media (max-width: 360px), (max-height: 230px) {
.pcol__plan {
width: 92px;
padding: 12px 8px;
}
.pcol__price {
font-size: 22px;
}
.pcol__note {
font-size: 10px;
}
.pcol__stamp {
width: 26px;
font-size: 15px;
top: -10px;
right: -8px;
}
}
@media (prefers-reduced-motion: reduce) {
.pcol__plan--pick {
animation: none !important;
transform: rotate(2deg);
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./PricingCollage.module.scss";
export type PricingCollagePlan = { tier: string; price: string; note: string };
export type PricingCollageProps = {
base?: PricingCollagePlan;
pick?: PricingCollagePlan;
stampIcon?: ReactNode;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PricingCollage({
base = { tier: "BASIC", price: "₩0", note: "부품 3개" },
pick = { tier: "STUDIO", price: "₩9", note: "부품 9개 + zip" },
stampIcon,
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#d93a1e",
}: PricingCollageProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.pcol} style={vars}>
<article className={`${styles.plan} ${styles.planBase}`}>
<p className={styles.tier}>{base.tier}</p>
<p className={styles.price}>{base.price}</p>
<p className={styles.note}>{base.note}</p>
</article>
<article className={`${styles.plan} ${styles.planPick}`}>
<span className={styles.stamp} aria-hidden="true">{stampIcon}</span>
<p className={styles.tier}>{pick.tier}</p>
<p className={styles.price}>{pick.price}</p>
<p className={styles.note}>{pick.note}</p>
</article>
</section>
);
}07 · Overlapping pricing layers — Measured area 5.199% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where to look in the code
When the front card lifts, its shadow has to grow from 5px to 17px or the stack stops reading as layers.
.pcol__plan--base { z-index: 1; transform: rotate(-3deg); }
.pcol__plan--pick {
z-index: 2;
margin-left: -20px; /* the front card covers the back card's corner */
margin-bottom: 16px;
}
@keyframes planLift {
0%, 100% { transform: rotate(2deg) translateY(0); box-shadow: 0 5px 0 rgba(36, 29, 46, .16); }
50% { transform: rotate(2deg) translateY(-12px); box-shadow: 0 17px 0 rgba(36, 29, 46, .12); }
}
08Stamp CTA band
What happens on screen
The bottom edge is scalloped out with two mask layers, and which half of the circle you keep decides everything.

<main class="stage">
<section class="bcol">
<p class="bcol__lead">All 9 parts at once</p>
<button class="bcol__btn" type="button">
Get the kit
<span class="bcol__arrow" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M216,128l-72,72V56Z" opacity="0.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,221.66,122.34ZM152,180.69V75.31L204.69,128Z"/></svg>
</span>
</button>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 콜라주·맥시멀 팔레트 (01 과 같은 값). 코랄 면 위 흰 글자 4.59:1
$cg-cream: #fbf3e4;
$cg-sheet: #ffffff;
$cg-ink: #241d2e;
$cg-coral: #bb7f74;
$cg-lemon: #d1c8a3;
$serif: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
$duration: $dur-loop; // 버튼이 도장처럼 눌렸다 떠오르는 한 주기
$easing: $ease-spring;
$color: $cg-coral; // 띠 바탕색
$scallop: 8px; // 아래쪽에서 파내는 반원의 반지름
@include stage($cg-cream);
.bcol {
box-sizing: border-box;
width: clamp(232px, 78vw, 304px);
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-3;
padding: $sp-5 $sp-4 calc(#{$scallop} + #{$sp-5});
border-radius: $r-xs $r-xs 0 0;
background: $cg-coral;
transform: rotate(-1.5deg);
// 아래 가장자리를 반원 물결로 — 이미지가 아니라 마스크 두 겹.
// 원 안쪽을 남기면 부채꼴이 아래로 매달리고(물결), 바깥을 남기면 톱니가 된다
mask:
radial-gradient(circle #{$scallop} at 50% 0, #000 98%, #0000 100%) 0 100% / #{$scallop * 2} #{$scallop} repeat-x,
linear-gradient(#000 0 0) 0 0 / 100% calc(100% - #{$scallop}) no-repeat;
}
.bcol__lead {
margin: 0;
color: $cg-sheet;
font-family: $serif;
font-weight: 700;
font-size: clamp(16px, 4.2vw, 21px);
line-height: 1.2;
}
.bcol__btn {
display: inline-flex;
align-items: center;
gap: $sp-2;
border: 0;
border-radius: $r-pill;
background: $cg-lemon;
color: $cg-ink;
padding: $sp-3 $sp-5;
font-family: inherit;
font-weight: 700;
font-size: clamp(12px, 3vw, 14px);
line-height: 1;
box-shadow: 0 8px 0 rgba($cg-ink, .35);
animation-name: stampPush;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.bcol__btn:focus-visible {
outline: 3px solid $cg-sheet;
outline-offset: 3px;
}
.bcol__arrow { font-size: 16px; line-height: 1; }
// 데모: 8px 두께가 눌린 만큼 사라진다 — 그림자가 두께라는 약속을 지킨다
@keyframes stampPush {
0%, 100% { transform: translateY(-6px) rotate(-2deg); box-shadow: 0 14px 0 rgba($cg-ink, .35); }
50% { transform: translateY(0) rotate(1.5deg); box-shadow: 0 2px 0 rgba($cg-ink, .35); }
}
@media (max-width: 360px), (max-height: 230px) {
.bcol { width: 224px; padding: $sp-3 $sp-3 calc(#{$scallop} + #{$sp-3}); gap: $sp-2; }
.bcol__lead { font-size: 14px; }
.bcol__btn { padding: $sp-2 $sp-4; }
}
@media (prefers-reduced-motion: reduce) {
.bcol__btn { animation: none !important; transform: rotate(-2deg); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf3e4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.bcol {
box-sizing: border-box;
width: clamp(232px, 78vw, 304px);
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 20px 16px calc(8px + 20px);
border-radius: 4px 4px 0 0;
background: #bb7f74;
transform: rotate(-1.5deg);
mask: radial-gradient(circle 8px at 50% 0, #000 98%, rgba(0, 0, 0, 0) 100%) 0 100%/16px 8px repeat-x, linear-gradient(#000 0 0) 0 0/100% calc(100% - 8px) no-repeat;
}
.bcol__lead {
margin: 0;
color: #ffffff;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: clamp(16px, 4.2vw, 21px);
line-height: 1.2;
}
.bcol__btn {
display: inline-flex;
align-items: center;
gap: 8px;
border: 0;
border-radius: 999px;
background: #d1c8a3;
color: #241d2e;
padding: 12px 20px;
font-family: inherit;
font-weight: 700;
font-size: clamp(12px, 3vw, 14px);
line-height: 1;
box-shadow: 0 8px 0 rgba(36, 29, 46, 0.35);
animation-name: stampPush;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.bcol__btn:focus-visible {
outline: 3px solid #ffffff;
outline-offset: 3px;
}
.bcol__arrow {
font-size: 16px;
line-height: 1;
}
@keyframes stampPush {
0%, 100% {
transform: translateY(-6px) rotate(-2deg);
box-shadow: 0 14px 0 rgba(36, 29, 46, 0.35);
}
50% {
transform: translateY(0) rotate(1.5deg);
box-shadow: 0 2px 0 rgba(36, 29, 46, 0.35);
}
}
@media (max-width: 360px), (max-height: 230px) {
.bcol {
width: 224px;
padding: 12px 12px calc(8px + 12px);
gap: 8px;
}
.bcol__lead {
font-size: 14px;
}
.bcol__btn {
padding: 8px 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.bcol__btn {
animation: none !important;
transform: rotate(-2deg);
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./CtaBandCollage.module.scss";
export type CtaBandCollageProps = {
lead?: string;
cta?: string;
arrowIcon?: ReactNode;
/** 아래 가장자리 물결의 반지름(px) */
scallop?: number;
onClick?: () => void;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string; "--easing"?: string; "--color"?: string; "--scallop"?: string;
};
export default function CtaBandCollage({
lead = "부품 9개, 한 번에",
cta = "키트 받기",
arrowIcon,
scallop = 8,
onClick,
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#d93a1e",
}: CtaBandCollageProps) {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
"--scallop": `${scallop}px`,
};
return (
<section className={styles.bcol} style={vars}>
<p className={styles.lead}>{lead}</p>
<button type="button" className={styles.btn} onClick={onClick}>
{cta}
<span className={styles.arrow} aria-hidden="true">{arrowIcon}</span>
</button>
</section>
);
}08 · Stamp CTA band — Measured area 2.449% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password
How to read the code
The button tilts as it presses, and its shadow thins from 14px to 2px on the way down.
.bcol {
mask:
radial-gradient(circle 8px at 50% 0, #000 98%, #0000 100%) 0 100% / 16px 8px repeat-x,
linear-gradient(#000 0 0) 0 0 / 100% calc(100% - 8px) no-repeat;
}
@keyframes stampPush {
0%, 100% { transform: translateY(-6px) rotate(-2deg); box-shadow: 0 14px 0 rgba(36, 29, 46, .35); }
50% { transform: translateY(0) rotate(1.5deg); box-shadow: 0 2px 0 rgba(36, 29, 46, .35); }
}
09Collage footer
What moves
Let a huge serif word run off the edge and the eye stops reading it as text and starts reading it as a surface.

<main class="stage">
<footer class="fcol">
<span class="fcol__word" aria-hidden="true">COLLAGE</span>
<p class="fcol__brand">The cut-and-paste kit</p>
<span class="fcol__rule" aria-hidden="true"></span>
<div class="fcol__cols">
<ul class="fcol__list">
<li>Parts</li>
<li>One-pager</li>
</ul>
<ul class="fcol__list">
<li>Tokens</li>
<li>Prompt</li>
</ul>
<ul class="fcol__list">
<li>zip</li>
<li>Contact</li>
</ul>
</div>
<div class="fcol__chips" aria-hidden="true">
<span class="fcol__chip fcol__chip--coral"></span>
<span class="fcol__chip fcol__chip--lemon"></span>
<span class="fcol__chip fcol__chip--lime"></span>
<span class="fcol__chip fcol__chip--sky">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" fill="currentColor"><path d="M223.69,42.18l-58.22,192a8,8,0,0,1-14.92,1.25L108,148,20.58,105.45a8,8,0,0,1,1.25-14.92l192-58.22A8,8,0,0,1,223.69,42.18Z" opacity="0.2"/><path d="M227.32,28.68a16,16,0,0,0-15.66-4.08l-.15,0L19.57,82.84a16,16,0,0,0-2.49,29.8L102,154l41.3,84.87A15.86,15.86,0,0,0,157.74,248q.69,0,1.38-.06a15.88,15.88,0,0,0,14-11.51l58.2-191.94c0-.05,0-.1,0-.15A16,16,0,0,0,227.32,28.68ZM157.83,231.85l-.05.14,0-.07-40.06-82.3,48-48a8,8,0,0,0-11.31-11.31l-48,48L24.08,98.25l-.07,0,.14,0L216,40Z"/></svg>
</span>
</div>
</footer>
</main>@use "../../../../site/static/demo-tokens" as *;
// 콜라주·맥시멀 팔레트 (01 과 같은 값)
$cg-cream: #fbf3e4;
$cg-sheet: #ffffff;
$cg-ink: #241d2e;
$cg-slate: #57506a;
$cg-coral: #bb7f74;
$cg-sky: #668cb4;
$cg-lemon: #d1c8a3;
$cg-lime: #bfce9c;
$serif: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
$duration: $dur-loop; // 조각 하나가 떴다 내려오는 한 주기
$easing: $ease-spring;
$color: $cg-coral; // 첫 조각 색
@include stage($cg-cream);
.fcol {
position: relative;
box-sizing: border-box;
width: clamp(232px, 80vw, 312px);
overflow: hidden;
padding: $sp-5 $sp-4 $sp-4;
border-radius: $r-xs;
background: $cg-sheet;
box-shadow: 0 5px 0 rgba($cg-ink, .16);
isolation: isolate;
}
// 배경 워드마크 — 일부러 잘리게 둔다. 글자가 아니라 면으로 읽힌다
.fcol__word {
position: absolute;
z-index: 0;
left: -$sp-3;
bottom: -$sp-4;
color: rgba($cg-ink, .1);
font-family: $serif;
font-weight: 700;
font-size: 62px;
line-height: 1;
letter-spacing: -.02em;
white-space: nowrap;
}
.fcol__brand {
position: relative;
z-index: 1;
margin: 0;
color: $cg-ink;
font-family: $serif;
font-weight: 700;
font-size: 15px;
line-height: 1;
transform: rotate(-1.5deg);
}
.fcol__rule {
position: relative;
z-index: 1;
display: block;
height: 3px;
margin: $sp-3 0;
border-radius: $r-xs;
background: repeating-linear-gradient(90deg, $cg-coral 0 10px, transparent 10px 18px);
}
.fcol__cols {
position: relative;
z-index: 1;
display: flex;
gap: $sp-6;
}
.fcol__list {
margin: 0;
padding: 0;
list-style: none;
color: $cg-slate;
font-weight: 600;
font-size: 12px;
line-height: 1.8;
}
.fcol__chips {
position: relative;
z-index: 2;
display: flex;
gap: $sp-2;
margin-top: $sp-4;
}
.fcol__chip {
width: 30px;
height: 22px;
display: grid;
place-items: center;
border-radius: $r-xs;
color: $cg-sheet;
font-size: 13px;
box-shadow: 0 3px 0 rgba($cg-ink, .18);
animation-name: chipBob;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.fcol__chip--coral { --rot: -8deg; background: $cg-coral; }
.fcol__chip--lemon { --rot: 6deg; background: $cg-lemon; animation-delay: -.5s; }
.fcol__chip--lime { --rot: -4deg; background: $cg-lime; animation-delay: -1s; }
.fcol__chip--sky { --rot: 9deg; background: $cg-sky; animation-delay: -1.5s; }
// 데모: 네 조각이 반 박자씩 어긋나 떠다닌다 — 붙인 각도는 그대로
@keyframes chipBob {
0%, 100% { transform: translateY(0) rotate(var(--rot)); }
50% { transform: translateY(-9px) rotate(calc(var(--rot) * -1)); }
}
@media (max-width: 360px), (max-height: 230px) {
.fcol { width: 236px; padding: $sp-3; }
.fcol__word { font-size: 44px; }
.fcol__brand { font-size: 12px; }
.fcol__rule { margin: $sp-2 0; }
.fcol__cols { gap: $sp-4; }
.fcol__list { font-size: 11px; line-height: 1.6; }
.fcol__chips { margin-top: $sp-2; }
.fcol__chip { width: 24px; height: 18px; font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.fcol__chip { animation: none !important; transform: rotate(var(--rot)); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf3e4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.fcol {
position: relative;
box-sizing: border-box;
width: clamp(232px, 80vw, 312px);
overflow: hidden;
padding: 20px 16px 16px;
border-radius: 4px;
background: #ffffff;
box-shadow: 0 5px 0 rgba(36, 29, 46, 0.16);
isolation: isolate;
}
.fcol__word {
position: absolute;
z-index: 0;
left: -12px;
bottom: -16px;
color: rgba(36, 29, 46, 0.1);
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: 62px;
line-height: 1;
letter-spacing: -0.02em;
white-space: nowrap;
}
.fcol__brand {
position: relative;
z-index: 1;
margin: 0;
color: #241d2e;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: 15px;
line-height: 1;
transform: rotate(-1.5deg);
}
.fcol__rule {
position: relative;
z-index: 1;
display: block;
height: 3px;
margin: 12px 0;
border-radius: 4px;
background: repeating-linear-gradient(90deg, #bb7f74 0 10px, transparent 10px 18px);
}
.fcol__cols {
position: relative;
z-index: 1;
display: flex;
gap: 24px;
}
.fcol__list {
margin: 0;
padding: 0;
list-style: none;
color: #57506a;
font-weight: 600;
font-size: 12px;
line-height: 1.8;
}
.fcol__chips {
position: relative;
z-index: 2;
display: flex;
gap: 8px;
margin-top: 16px;
}
.fcol__chip {
width: 30px;
height: 22px;
display: grid;
place-items: center;
border-radius: 4px;
color: #ffffff;
font-size: 13px;
box-shadow: 0 3px 0 rgba(36, 29, 46, 0.18);
animation-name: chipBob;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.fcol__chip--coral {
--rot: -8deg;
background: #bb7f74;
}
.fcol__chip--lemon {
--rot: 6deg;
background: #d1c8a3;
animation-delay: -0.5s;
}
.fcol__chip--lime {
--rot: -4deg;
background: #bfce9c;
animation-delay: -1s;
}
.fcol__chip--sky {
--rot: 9deg;
background: #668cb4;
animation-delay: -1.5s;
}
@keyframes chipBob {
0%, 100% {
transform: translateY(0) rotate(var(--rot));
}
50% {
transform: translateY(-9px) rotate(calc(var(--rot) * -1));
}
}
@media (max-width: 360px), (max-height: 230px) {
.fcol {
width: 236px;
padding: 12px;
}
.fcol__word {
font-size: 44px;
}
.fcol__brand {
font-size: 12px;
}
.fcol__rule {
margin: 8px 0;
}
.fcol__cols {
gap: 16px;
}
.fcol__list {
font-size: 11px;
line-height: 1.6;
}
.fcol__chips {
margin-top: 8px;
}
.fcol__chip {
width: 24px;
height: 18px;
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.fcol__chip {
animation: none !important;
transform: rotate(var(--rot));
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./FooterCollage.module.scss";
export type FooterCollageProps = {
word?: string;
brand?: string;
columns?: string[][];
/** 조각끼리 어긋나는 간격(ms) */
stagger?: number;
planeIcon?: ReactNode;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type ChipVars = CSSProperties & { "--rot"?: string; background?: string; animationDelay?: string };
const defaultColumns: string[][] = [
["부품", "원페이지"],
["토큰", "프롬프트"],
["zip", "문의"],
];
const pieces = [
{ bg: "#d93a1e", rot: "-8deg" },
{ bg: "#ffd93d", rot: "6deg" },
{ bg: "#b6e34f", rot: "-4deg" },
{ bg: "#2d6fb5", rot: "9deg" },
];
export default function FooterCollage({
word = "COLLAGE",
brand = "오려 붙이는 랜딩 키트",
columns = defaultColumns,
stagger = 500,
planeIcon,
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#d93a1e",
}: FooterCollageProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<footer className={styles.fcol} style={vars}>
<span className={styles.word} aria-hidden="true">{word}</span>
<p className={styles.brand}>{brand}</p>
<span className={styles.rule} aria-hidden="true" />
<div className={styles.cols}>
{columns.map((col, i) => (
<ul key={i} className={styles.list}>
{col.map((x) => <li key={x}>{x}</li>)}
</ul>
))}
</div>
<div className={styles.chips} aria-hidden="true">
{pieces.map((c, i) => {
const cv: ChipVars = { "--rot": c.rot, background: c.bg, animationDelay: `${-stagger * i}ms` };
return (
<span key={c.bg} className={styles.chip} style={cv}>
{i === pieces.length - 1 ? planeIcon : null}
</span>
);
})}
</div>
</footer>
);
}09 · Collage footer — Measured area 2.096% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
What to change
The four chips in front bob half a second apart, flipping the sign of their tilt on the way up.
.fcol__word {
position: absolute;
left: -12px; bottom: -16px;
color: rgba(36, 29, 46, .1); /* 10% keeps it a surface, not a headline */
font-size: 62px;
white-space: nowrap;
}
@keyframes chipBob {
0%, 100% { transform: translateY(0) rotate(var(--rot)); }
50% { transform: translateY(-9px) rotate(calc(var(--rot) * -1)); }
}
Custom tailwind.config
The value worth centralizing here isn't color — it's angle. Pick a fresh tilt for every element and the overlap reads as a mistake instead of a decision, so the config turns four angles into named utilities and stops there.
theme: {
extend: {
colors: {
cream: "#fbf3e4", sheet: "#ffffff", ink: "#241d2e", slate: "#57506a",
coral: "#d93a1e", sky: "#2d6fb5", grape: "#6b3fa0",
lemon: "#ffd93d", lime: "#b6e34f", blush: "#ffb3c7",
},
borderRadius: { chip: "4px", control: "12px", card: "16px", pill: "999px" },
boxShadow: {
paste: "0 5px 0 rgba(36,29,46,.16)", /* resting on the surface = thickness */
stamp: "0 4px 0 rgba(36,29,46,.2)",
lift: "0 16px 0 rgba(36,29,46,.12)", /* only while it's raised */
},
rotate: { a: "-3deg", b: "2deg", c: "-1.5deg", d: "5deg" },
},
},
| Role | Token | When |
|---|---|---|
| Pasted angle | rotate-a through rotate-d |
Frames, cards, badges, bands — all of them |
| Resting thickness | shadow-paste |
Anything sitting on the surface |
| Raised | shadow-lift |
Only during the lift |
Vibe-coding prompt
With the config in place, one block of instructions is all your AI editor needs. Putting the angle list ahead of the color list is the part that matters. The full, untrimmed text ships as prompt.md inside the zip.
Use exactly four angles: -3deg, 2deg, -1.5deg, 5deg. Never invent one outside that list.
Anything resting on the surface gets a zero-blur shadow, 0 5px 0 rgba(36,29,46,.16);
swap it for 0 16px 0 only while the element is raised. One background tone,
cream (#fbf3e4); one surface tone, sheet (#ffffff). White text goes on coral, sky, and
grape only; lemon, lime, and blush carry ink text only. Use no photographs — a photo slot
is a two-stop linear-gradient inside a 6px white border.
The screenshot below is the generated page/index.html, captured as a single full-page shot at a 1280px viewport.

Where it breaks (the trap)
The edge of the CTA band cost the most time. To scallop the bottom I laid down a radial-gradient mask, and the first version — circle 8px at 50% 0, #0000 98%, #000 100% — kept the outside of each circle, hanging a row of sharp downward triangles off the band. The poster made it obvious: those were teeth, not waves. Flipping the color stops to #000 98%, #0000 100% keeps the inside instead, and the half-circles finally dangled the right way. Read a mask as "what stays," never as "what gets removed."
The marquee was the second trap. I tilted the two bands -2.5 and 2 degrees and overlapped them with a negative margin-top, and the render came back with the lower band sitting across the baseline of the upper band's letters. A rotated element occupies more vertical space than its pre-rotation height, so an overlap computed from the unrotated box doesn't hold. Narrowing the lower band to 88% and pushing it right, so only the band edges meet, made both rows readable again. While I was in there I re-measured the track with Playwright's offsetWidth; getBoundingClientRect reports the post-rotation box and is useless for this. Upper track 796px against a 398px set, lower track 524px against 262px — both exactly half, so one lap of translateX(-50%) leaves a 0px seam. Moving a layer-heavy screen into the inbox needs the same discipline, and Marketing Email Design covers the Outlook-breaking bar. The files with both traps fixed are inside the zip, which opens with the password s3yjtjja.
Accessibility and reduced-motion
The more colors a palette carries, the less you can judge contrast by eye. All ten were run through the relative-luminance formula from the WCAG 2.1 minimum contrast requirement. Body text needs 4.5:1.
| Color | Value | On cream | On white | With ink text on it |
|---|---|---|---|---|
| Ink | #241d2e |
14.76:1 | 16.28:1 | — |
| Slate | #57506a |
6.90:1 | 7.61:1 | — |
| Coral | #d93a1e |
4.16:1 | 4.59:1 | 3.55:1 |
| Sky | #2d6fb5 |
4.70:1 | 5.18:1 | 3.14:1 |
| Grape | #6b3fa0 |
6.70:1 | 7.38:1 | 2.20:1 |
| Lemon | #ffd93d |
1.25:1 | 1.38:1 | 11.82:1 |
| Lime | #b6e34f |
1.35:1 | 1.49:1 | 10.92:1 |
| Blush | #ffb3c7 |
1.52:1 | 1.68:1 | 9.71:1 |
Two rules fall out of that table. Coral clears the bar as text on white at 4.59:1 but lands at 4.16:1 on the cream background, so every coral word across the nine parts lives inside a white card. Lemon, lime, and blush go the other way: unusable as text anywhere, yet over 9:1 once ink sits on them, which is why they only ever appear as the surface of a badge or a band.
For motion, the media query from MDN's prefers-reduced-motion page is in all nine stylesheets. With reduce turned on, the swaying and the scrolling stop, but the pasted angles stay. The tilt built with rotate from MDN's transform page is the identity of this style, and zeroing it out would hand the visitor a different design rather than a calmer one.
For a lighter take on the same bright palette, the paper cutout landing kit is the closest neighbor, and the about page explains what this site ships with every post.
FAQ
Won't six colors on one screen look messy?
Messiness comes from overlapping roles, not from the count. Coral, sky, and grape are text-bearing surfaces here; lemon, lime, and blush never carry text. Once the roles are split, the eye groups six colors into two families and stops counting. Where a collage has to argue for trust, Testimonial Section Design is the part that fits.
Why cap the angles at four?
Because tilt is a list, not a value. Hand out -2, -7, and 3 degrees element by element and a reader files it under "misaligned" rather than "deliberately tilted." Four angles in rotation look like one hand pasted everything down.
Do the frames in this editorial web design look empty without real photos?
They would if you left the slot blank, but a two-stop gradient inside a 6px white border reads as something being framed. Dropping in a real image later means changing background and nothing else, so nobody has to wait for asset delivery to review the layout.









