클레이모피즘 랜딩 UI 키트, 통통한 섹션 9개
클레이모피즘은 두꺼운 모서리 반경과 밝고 어두운 그림자를 한 면에 같이 얹어, 화면을 손으로 눌러 놓은 찰흙처럼 보이게 하는 디자인 스타일입니다.
핀터레스트·드리블 보드에 계속 다시 올라오는 룩이라, 실제로 만든 원페이지를 아래에 그대로 올렸습니다. 내비게이션에서 푸터까지 쭉 스크롤하면서 전체 인상을 먼저 확인하십시오.

클레이모피즘 만들기에 쓴 조각 아홉 개를 낱개로 분리해 아래 격자에 다시 배열했습니다. 인기순이 아니라 화면을 처음 여는 사람이 실제로 마주치는 순서—시선을 끌기, 둘러보기, 가격 확인, 신뢰, 행동 유도, 가입—를 따라갑니다. 아홉 개 중 세 개(히어로·가격·버튼과 입력창)는 흔한 카드 나열이 아니라 눌림·부유 같은 동작이 붙은 아이디어형입니다. 같은 화면을 글자의 움직임으로 끌고 가면 키네틱 타이포그래피 랜딩이 되고, 덩어리 대신 빛의 번짐으로 채우면 오로라 그라디언트 랜딩 부품이 이웃입니다.
01 hero-clay
02 nav-clay
03 card-grid-clay
04 feature-list-clay
05 pricing-clay
06 testimonial-clay
07 cta-band-clay
08 footer-clay
09 buttons-inputs-clay
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 찰흙 도형 히어로
- 02 캡슐 내비
- 03 부풀린 카드
- 04 둥근 아이콘 칩
- 05 통통한 플랜 카드
- 06 둥근 아바타+말풍선
- 07 파스텔 CTA 밴드
- 08 둥근 상단 푸터
- 09 찰흙 버튼·인풋
01찰흙 도형 히어로
블롭과 눌린 헤드라인 카드
배경에 뜬 유기형 블롭 두 개가 천천히 흔들리고, 헤드라인 카드는 밝은 안쪽 하이라이트와 짙은 바깥 그림자로 눌린 찰흙처럼 붙습니다.

<main class="stage">
<section class="hero">
<div class="hero__blob hero__blob--a is-demo" aria-hidden="true"></div>
<div class="hero__blob hero__blob--b is-demo" aria-hidden="true"></div>
<div class="hero__card is-demo">
<p class="hero__eyebrow">Claymolt</p>
<h1 class="hero__title">말랑한 시작</h1>
<p class="hero__desc">눌러도 돌아와요</p>
<button class="hero__cta" type="button">무료로 시작</button>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 블롭이 한 바퀴 떠도는 시간
$easing: $ease-spring; // 떠오르는 곡선
$color: $subject-blue; // 강조 색
@include stage($stage-ink);
.hero {
position: relative;
width: clamp(240px, 82vw, 380px);
}
.hero__blob {
position: absolute;
width: clamp(110px, 42vw, 170px);
aspect-ratio: 1;
pointer-events: none;
}
.hero__blob--a {
top: -14px;
left: -14px;
background: rgba($color, .35);
border-radius: 42% 58% 63% 37% / 41% 44% 56% 59%;
animation: blobA $duration $easing infinite;
}
.hero__blob--b {
bottom: -16px;
right: -12px;
background: rgba($subject-cream, .3);
border-radius: 58% 42% 39% 61% / 56% 63% 37% 44%;
animation: blobB $duration $easing infinite;
animation-delay: -1s;
}
.hero__card {
position: relative;
box-sizing: border-box;
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $sp-1;
padding: $sp-3;
border-radius: $r-card;
background: $subject-cream;
box-shadow: 8px 8px 16px rgba($stage-ink, .3), -8px -8px 16px rgba($stage-paper, .5);
animation: cardRise $duration $easing infinite;
}
.hero__eyebrow {
margin: 0;
color: $subject-blue-deep; // 밝은 면 위 파란 글자 — AA 4.5
font-weight: 700; font-size: clamp(10px, 2.6vw, 12px); line-height: 1;
letter-spacing: .04em;
text-transform: uppercase;
}
.hero__title {
margin: 0;
color: $subject-ink;
font-weight: 700; font-size: clamp(18px, 4.6vw, 24px); line-height: 1.2;
}
.hero__desc {
margin: 0;
color: rgba($subject-ink, .7);
font-weight: 500; font-size: clamp(12px, 2.8vw, 14px); line-height: 1.4;
}
.hero__cta {
margin-top: $sp-1;
border: 0;
border-radius: $r-pill;
background: $subject-ink;
color: $subject-cream;
padding: $sp-1 $sp-5;
font-weight: 600; font-size: clamp(12px, 2.8vw, 14px); line-height: 1;
box-shadow: 4px 4px 8px rgba($stage-ink, .3);
}
// 데모: 블롭 두 개가 서로 반대 위상으로 떠돌고, 카드는 살짝 떠올랐다 가라앉는다 (WAAPI 시계가 잡을 수 있게 keyframes)
@keyframes blobA {
0%, 100% { transform: translate(0, 0) rotate(0deg) scale(1); }
50% { transform: translate(4%, 6%) rotate(10deg) scale(1.08); }
}
@keyframes blobB {
0%, 100% { transform: translate(0, 0) rotate(0deg) scale(1); }
50% { transform: translate(-5%, -4%) rotate(-8deg) scale(.93); }
}
@keyframes cardRise {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@media (prefers-reduced-motion: reduce) {
.hero__blob, .hero__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: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.hero {
position: relative;
width: clamp(240px, 82vw, 380px);
}
.hero__blob {
position: absolute;
width: clamp(110px, 42vw, 170px);
aspect-ratio: 1;
pointer-events: none;
}
.hero__blob--a {
top: -14px;
left: -14px;
background: rgba(47, 109, 246, 0.35);
border-radius: 42% 58% 63% 37%/41% 44% 56% 59%;
animation: blobA 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.hero__blob--b {
bottom: -16px;
right: -12px;
background: rgba(255, 247, 230, 0.3);
border-radius: 58% 42% 39% 61%/56% 63% 37% 44%;
animation: blobB 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: -1s;
}
.hero__card {
position: relative;
box-sizing: border-box;
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
padding: 12px;
border-radius: 16px;
background: #fff7e6;
box-shadow: 8px 8px 16px rgba(23, 20, 26, 0.3), -8px -8px 16px rgba(255, 255, 255, 0.5);
animation: cardRise 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.hero__eyebrow {
margin: 0;
color: #0a4fe8;
font-weight: 700;
font-size: clamp(10px, 2.6vw, 12px);
line-height: 1;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.hero__title {
margin: 0;
color: #17141a;
font-weight: 700;
font-size: clamp(18px, 4.6vw, 24px);
line-height: 1.2;
}
.hero__desc {
margin: 0;
color: rgba(23, 20, 26, 0.7);
font-weight: 500;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.4;
}
.hero__cta {
margin-top: 4px;
border: 0;
border-radius: 999px;
background: #17141a;
color: #fff7e6;
padding: 4px 20px;
font-weight: 600;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1;
box-shadow: 4px 4px 8px rgba(23, 20, 26, 0.3);
}
@keyframes blobA {
0%, 100% {
transform: translate(0, 0) rotate(0deg) scale(1);
}
50% {
transform: translate(4%, 6%) rotate(10deg) scale(1.08);
}
}
@keyframes blobB {
0%, 100% {
transform: translate(0, 0) rotate(0deg) scale(1);
}
50% {
transform: translate(-5%, -4%) rotate(-8deg) scale(0.93);
}
}
@keyframes cardRise {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-6px);
}
}
@media (prefers-reduced-motion: reduce) {
.hero__blob, .hero__card {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./HeroClay.module.scss";
export type HeroClayProps = {
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 HeroClay({
eyebrow = "Claymolt",
title = "말랑한 시작",
desc = "눌러도 돌아와요",
cta = "무료로 시작",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: HeroClayProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.hero} style={vars}>
<div className={`${styles.blob} ${styles.blobA}`} aria-hidden="true" />
<div className={`${styles.blob} ${styles.blobB}`} 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>
</section>
);
}01 · 찰흙 도형 히어로 — 측정 누적면적 16.283% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
형태는 정지, 움직임은 transform
블롭은 border-radius를 퍼센트 여러 개로 섞어 정지 상태에서 만들고, 움직임은 transform만 맡습니다.
<div class="relative mx-auto max-w-xl flex flex-col items-start gap-4
rounded-clay bg-white p-10 shadow-clay-raised">
<p class="text-xs font-bold uppercase text-accent">Claymolt</p>
<h1 class="text-4xl font-extrabold leading-tight">말랑하게 시작하는 화면</h1>
<button class="rounded-pill bg-ink px-6 py-3 text-sm font-bold text-cream">
무료로 시작
</button>
</div>
02캡슐 내비
알약 캡슐 안의 메뉴
메뉴 전체가 알약 모양 캡슐에 담기고, 현재 위치 칩은 안쪽으로 파인 트랙 위에서 옆 칸으로 미끄러집니다.

<main class="stage">
<nav class="navclay is-demo">
<span class="navclay__brand">Claymolt</span>
<div class="navclay__menu">
<span class="navclay__indicator" aria-hidden="true"></span>
<span class="navclay__item">홈</span>
<span class="navclay__item">기능</span>
<span class="navclay__item">요금제</span>
<span class="navclay__item">문의</span>
</div>
<button class="navclay__cta" type="button">시작하기</button>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 인디케이터가 4칸을 한 바퀴 도는 시간
$easing: $ease-spring; // 미끄러지는 곡선
$color: $subject-ink; // 강조 색 (노랑 무대라 먹색으로 대비)
@include stage($stage-yellow);
.navclay {
box-sizing: border-box;
width: clamp(260px, 84vw, 400px);
display: flex;
align-items: center;
gap: $sp-3;
border-radius: $r-pill;
background: $subject-cream;
padding: $sp-2;
box-shadow: 6px 6px 14px rgba($stage-ink, .22), -6px -6px 14px rgba($stage-paper, .6);
}
.navclay__brand {
padding-left: $sp-2;
color: $color;
font-weight: 800; font-size: clamp(11px, 2.8vw, 13px); line-height: 1;
white-space: nowrap;
}
.navclay__menu {
position: relative;
flex: 1;
display: flex;
border-radius: $r-pill;
background: rgba($stage-ink, .06);
padding: $sp-1;
box-shadow: inset 3px 3px 6px rgba($stage-ink, .18), inset -3px -3px 6px rgba($stage-paper, .55);
}
.navclay__indicator {
position: absolute;
top: $sp-1;
bottom: $sp-1;
left: $sp-1;
// 실측 함정: width:25% 는 절대배치 기준 박스(.navclay__menu 의 padding box, 208px)로 계산되는데
// 실제 항목 4개는 그 안쪽 content box(200px, 부모 padding 8px 뺀 값)로 나뉜다 — 인디케이터가 항목보다
// 정확히 2px씩 넓어져 translateX(100%) 마다 어긋남이 누적됐다(1→3→5→7px 실측). 같은 content box 기준으로 계산
width: calc((100% - #{$sp-1} * 2) / 4);
border-radius: $r-pill;
// 밝은 캡슐로 둔다 — 먹색이면 먹색 글자와 겹칠 때 라벨이 안 보인다 (실측: 정적 스크린샷에서 "홈" 소실)
background: $subject-cream;
box-shadow: 2px 2px 6px rgba($stage-ink, .25), inset 1px 1px 0 rgba($stage-paper, .6);
animation: navSlide $duration $easing infinite;
}
.navclay__item {
position: relative;
z-index: 1;
flex: 1;
text-align: center;
padding: $sp-2 0;
color: $subject-ink;
font-weight: 600; font-size: clamp(10.5px, 2.6vw, 12px); line-height: 1;
}
.navclay__cta {
border: 0;
border-radius: $r-pill;
background: $color;
color: $subject-cream;
padding: $sp-2 $sp-4;
font-weight: 700; font-size: clamp(10.5px, 2.6vw, 12px); line-height: 1;
white-space: nowrap;
}
// 데모: 캡슐 안 인디케이터가 4칸을 순서대로 돈다. width 25% 이므로 translateX(100%) = 제 폭만큼 = 한 칸
@keyframes navSlide {
0%, 15% { transform: translateX(0%); }
25%, 40% { transform: translateX(100%); }
50%, 65% { transform: translateX(200%); }
75%, 90% { transform: translateX(300%); }
100% { transform: translateX(0%); }
}
@media (prefers-reduced-motion: reduce) {
.navclay__indicator { 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: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.navclay {
box-sizing: border-box;
width: clamp(260px, 84vw, 400px);
display: flex;
align-items: center;
gap: 12px;
border-radius: 999px;
background: #fff7e6;
padding: 8px;
box-shadow: 6px 6px 14px rgba(23, 20, 26, 0.22), -6px -6px 14px rgba(255, 255, 255, 0.6);
}
.navclay__brand {
padding-left: 8px;
color: #17141a;
font-weight: 800;
font-size: clamp(11px, 2.8vw, 13px);
line-height: 1;
white-space: nowrap;
}
.navclay__menu {
position: relative;
flex: 1;
display: flex;
border-radius: 999px;
background: rgba(23, 20, 26, 0.06);
padding: 4px;
box-shadow: inset 3px 3px 6px rgba(23, 20, 26, 0.18), inset -3px -3px 6px rgba(255, 255, 255, 0.55);
}
.navclay__indicator {
position: absolute;
top: 4px;
bottom: 4px;
left: 4px;
width: calc((100% - 4px * 2) / 4);
border-radius: 999px;
background: #fff7e6;
box-shadow: 2px 2px 6px rgba(23, 20, 26, 0.25), inset 1px 1px 0 rgba(255, 255, 255, 0.6);
animation: navSlide 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.navclay__item {
position: relative;
z-index: 1;
flex: 1;
text-align: center;
padding: 8px 0;
color: #17141a;
font-weight: 600;
font-size: clamp(10.5px, 2.6vw, 12px);
line-height: 1;
}
.navclay__cta {
border: 0;
border-radius: 999px;
background: #17141a;
color: #fff7e6;
padding: 8px 16px;
font-weight: 700;
font-size: clamp(10.5px, 2.6vw, 12px);
line-height: 1;
white-space: nowrap;
}
@keyframes navSlide {
0%, 15% {
transform: translateX(0%);
}
25%, 40% {
transform: translateX(100%);
}
50%, 65% {
transform: translateX(200%);
}
75%, 90% {
transform: translateX(300%);
}
100% {
transform: translateX(0%);
}
}
@media (prefers-reduced-motion: reduce) {
.navclay__indicator {
animation: none !important;
transform: translateX(0%);
}
}import type { CSSProperties } from "react";
import styles from "./NavClay.module.scss";
export type NavClayProps = {
brand?: string;
items?: string[];
cta?: string;
activeIndex?: number;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function NavClay({
brand = "Claymolt",
items = ["홈", "기능", "요금제", "문의"],
cta = "시작하기",
activeIndex,
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#17141a",
}: NavClayProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const indicatorStyle: CSSProperties | undefined =
activeIndex !== undefined
? { animation: "none", transform: `translateX(${activeIndex * 100}%)` }
: undefined;
return (
<nav className={styles.nav} style={vars}>
<span className={styles.brand}>{brand}</span>
<div className={styles.menu}>
<span className={styles.indicator} style={indicatorStyle} aria-hidden="true" />
{items.map((label) => (
<span key={label} className={styles.item}>{label}</span>
))}
</div>
<button type="button" className={styles.cta}>{cta}</button>
</nav>
);
}02 · 캡슐 내비 — 측정 누적면적 4.142% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
칩 폭을 25%로 두는 이유
칩 폭을 25%로 두면 translateX(100%) 한 번이 정확히 이웃 칸 하나를 옮깁니다.
<nav class="flex items-center gap-4 rounded-pill bg-white/90 px-3 py-2
shadow-clay-raised">
<span class="pl-3 font-display font-extrabold text-sm">Claymolt</span>
<div class="relative flex flex-1 rounded-pill bg-ink/5 p-1 shadow-clay-press">
<span class="absolute inset-y-1 left-1 w-1/4 rounded-pill bg-cream
shadow-clay-raised"></span>
<span class="relative z-10 flex-1 text-center text-xs font-semibold py-2">홈</span>
</div>
</nav>
03부풀린 카드
배경에서 볼록 솟은 카드
격자의 카드마다 배경에서 볼록 솟아오른 듯 밝은 상단 하이라이트와 짙은 하단 그림자를 동시에 가집니다.

<main class="stage">
<div class="cardgrid">
<div class="cardgrid__item cardgrid__item--a is-demo">
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">빠른 검색</p>
</div>
<div class="cardgrid__item cardgrid__item--b is-demo">
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">자동 정리</p>
</div>
<div class="cardgrid__item cardgrid__item--c is-demo">
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">팀 채팅</p>
</div>
<div class="cardgrid__item cardgrid__item--d is-demo">
<span class="cardgrid__icon" aria-hidden="true"></span>
<p class="cardgrid__label">리포트</p>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 카드마다 부푸는 한 주기
$easing: $ease-spring; // 부푸는 곡선
$color: $subject-cream; // 무대(주황)와 대비되는 피사체 색
@include stage($stage-orange);
.cardgrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $sp-2;
width: clamp(220px, 78vw, 340px);
}
.cardgrid__item {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $sp-1;
padding: $sp-3;
border-radius: $r-card;
background: $color;
box-shadow: 8px 8px 16px rgba($stage-ink, .28), -8px -8px 16px rgba($stage-paper, .5);
animation: puff $duration $easing infinite;
}
.cardgrid__item--a { animation-delay: 0s; }
.cardgrid__item--b { animation-delay: -0.5s; }
.cardgrid__item--c { animation-delay: -1s; }
.cardgrid__item--d { animation-delay: -1.5s; }
.cardgrid__icon {
width: clamp(20px, 8vw, 28px);
height: clamp(20px, 8vw, 28px);
border-radius: $r-sm;
background: $subject-blue;
}
.cardgrid__label {
margin: 0;
color: $subject-ink;
font-weight: 700; font-size: clamp(11px, 2.8vw, 13px); line-height: 1.3;
}
// 데모: 네 칸이 서로 다른 박자로 살짝 부풀었다 가라앉는다
@keyframes puff {
0%, 100% { transform: scale(1) translateY(0); }
50% { transform: scale(1.05) translateY(-4px); }
}
@media (prefers-reduced-motion: reduce) {
.cardgrid__item { 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: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cardgrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
width: clamp(220px, 78vw, 340px);
}
.cardgrid__item {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
padding: 12px;
border-radius: 16px;
background: #fff7e6;
box-shadow: 8px 8px 16px rgba(23, 20, 26, 0.28), -8px -8px 16px rgba(255, 255, 255, 0.5);
animation: puff 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.cardgrid__item--a {
animation-delay: 0s;
}
.cardgrid__item--b {
animation-delay: -0.5s;
}
.cardgrid__item--c {
animation-delay: -1s;
}
.cardgrid__item--d {
animation-delay: -1.5s;
}
.cardgrid__icon {
width: clamp(20px, 8vw, 28px);
height: clamp(20px, 8vw, 28px);
border-radius: 8px;
background: #2f6df6;
}
.cardgrid__label {
margin: 0;
color: #17141a;
font-weight: 700;
font-size: clamp(11px, 2.8vw, 13px);
line-height: 1.3;
}
@keyframes puff {
0%, 100% {
transform: scale(1) translateY(0);
}
50% {
transform: scale(1.05) translateY(-4px);
}
}
@media (prefers-reduced-motion: reduce) {
.cardgrid__item {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./CardGridClay.module.scss";
export type CardGridClayItem = { label: string };
export type CardGridClayProps = {
items?: CardGridClayItem[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const DELAYS = ["0s", "-0.5s", "-1s", "-1.5s"];
export default function CardGridClay({
items = [
{ label: "빠른 검색" },
{ label: "자동 정리" },
{ label: "팀 채팅" },
{ label: "리포트" },
],
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#fff7e6",
}: CardGridClayProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.grid} style={vars}>
{items.map((it, i) => (
<div
key={it.label}
className={styles.item}
style={{ animationDelay: DELAYS[i % DELAYS.length] }}
>
<span className={styles.icon} aria-hidden="true" />
<p className={styles.label}>{it.label}</p>
</div>
))}
</div>
);
}03 · 부풀린 카드 — 측정 누적면적 16.755% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
파도처럼 순서대로
카드마다 애니메이션 시작 시각을 어긋내면 네 칸이 파도처럼 순서대로 부풀어 보입니다.
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div class="flex flex-col gap-3 rounded-clay bg-white p-5 shadow-clay-raised">
<span class="h-7 w-7 rounded-control bg-accent"></span>
<p class="text-sm font-semibold">빠른 검색</p>
</div>
</div>
04둥근 아이콘 칩
클레이모피즘 아이콘은 왜 파묻히나
아이콘마다 둥근 캡슐 칩 안에 얹혀 눌린 스탬프처럼 보이고, 설명 텍스트가 옆으로 붙습니다.

<main class="stage">
<ul class="featurelist">
<li class="featurelist__item featurelist__item--a is-demo">
<span class="featurelist__chip" aria-hidden="true"></span>
<span class="featurelist__text">빠른 시작</span>
</li>
<li class="featurelist__item featurelist__item--b is-demo">
<span class="featurelist__chip" aria-hidden="true"></span>
<span class="featurelist__text">자동 백업</span>
</li>
<li class="featurelist__item featurelist__item--c is-demo">
<span class="featurelist__chip" aria-hidden="true"></span>
<span class="featurelist__text">실시간 공유</span>
</li>
<li class="featurelist__item featurelist__item--d is-demo">
<span class="featurelist__chip" aria-hidden="true"></span>
<span class="featurelist__text">맞춤 알림</span>
</li>
</ul>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 칩이 밀려 들어왔다 나가는 한 주기
$easing: $ease-out; // 슬라이드 곡선
$color: $subject-blue; // 칩 강조 색
@include stage($stage-paper);
.featurelist {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: clamp(4px, 2vw, 8px);
width: clamp(220px, 74vw, 320px);
}
.featurelist__item {
display: flex;
align-items: center;
gap: $sp-2;
padding: clamp(4px, 1.5vw, 8px) $sp-2;
border-radius: $r-pill;
background: $subject-cream;
box-shadow: 6px 6px 12px rgba($stage-ink, .16), -6px -6px 12px rgba($stage-paper, .9);
animation: chipIn $duration $easing infinite;
}
.featurelist__item--a { animation-delay: 0s; }
.featurelist__item--b { animation-delay: -0.5s; }
.featurelist__item--c { animation-delay: -1s; }
.featurelist__item--d { animation-delay: -1.5s; }
.featurelist__chip {
flex: none;
width: clamp(24px, 8vw, 36px);
height: clamp(24px, 8vw, 36px);
border-radius: $r-pill;
background: $color;
box-shadow: inset 2px 2px 4px rgba($stage-ink, .2), inset -2px -2px 4px rgba($stage-paper, .5);
}
.featurelist__text {
color: $subject-ink;
font-weight: 600; font-size: clamp(12px, 2.8vw, 14px); line-height: 1.3;
}
// 데모: 칩마다 살짝 다른 박자로 오른쪽에서 밀려 들어왔다 나간다
@keyframes chipIn {
0% { opacity: .5; transform: translateX(-6px); }
25% { opacity: 1; transform: translateX(0); }
75% { opacity: 1; transform: translateX(0); }
100% { opacity: .5; transform: translateX(-6px); }
}
@media (prefers-reduced-motion: reduce) {
.featurelist__item { animation: none !important; opacity: 1; 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);
}
.featurelist {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: clamp(4px, 2vw, 8px);
width: clamp(220px, 74vw, 320px);
}
.featurelist__item {
display: flex;
align-items: center;
gap: 8px;
padding: clamp(4px, 1.5vw, 8px) 8px;
border-radius: 999px;
background: #fff7e6;
box-shadow: 6px 6px 12px rgba(23, 20, 26, 0.16), -6px -6px 12px rgba(255, 255, 255, 0.9);
animation: chipIn 2s ease-out infinite;
}
.featurelist__item--a {
animation-delay: 0s;
}
.featurelist__item--b {
animation-delay: -0.5s;
}
.featurelist__item--c {
animation-delay: -1s;
}
.featurelist__item--d {
animation-delay: -1.5s;
}
.featurelist__chip {
flex: none;
width: clamp(24px, 8vw, 36px);
height: clamp(24px, 8vw, 36px);
border-radius: 999px;
background: #2f6df6;
box-shadow: inset 2px 2px 4px rgba(23, 20, 26, 0.2), inset -2px -2px 4px rgba(255, 255, 255, 0.5);
}
.featurelist__text {
color: #17141a;
font-weight: 600;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.3;
}
@keyframes chipIn {
0% {
opacity: 0.5;
transform: translateX(-6px);
}
25% {
opacity: 1;
transform: translateX(0);
}
75% {
opacity: 1;
transform: translateX(0);
}
100% {
opacity: 0.5;
transform: translateX(-6px);
}
}
@media (prefers-reduced-motion: reduce) {
.featurelist__item {
animation: none !important;
opacity: 1;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./FeatureListClay.module.scss";
export type FeatureListClayProps = {
items?: string[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const DELAYS = ["0s", "-0.5s", "-1s", "-1.5s"];
export default function FeatureListClay({
items = ["빠른 시작", "자동 백업", "실시간 공유", "맞춤 알림"],
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
}: FeatureListClayProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<ul className={styles.list} style={vars}>
{items.map((text, i) => (
<li key={text} className={styles.item} style={{ animationDelay: DELAYS[i % DELAYS.length] }}>
<span className={styles.chip} aria-hidden="true" />
<span className={styles.text}>{text}</span>
</li>
))}
</ul>
);
}04 · 둥근 아이콘 칩 — 측정 누적면적 9.001% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
유일하게 오목한 표면
칩 배경에는 안쪽 그림자만 줘서, 튀어나온 카드들 사이에서 유일하게 파묻힌 표면으로 대비를 만듭니다.
<ul class="flex flex-col gap-3">
<li class="flex items-center gap-3 rounded-pill bg-white p-2 pr-5
shadow-clay-raised">
<span class="h-9 w-9 flex-none rounded-pill bg-accent
shadow-clay-press"></span>
<span class="text-sm font-semibold">빠른 시작</span>
</li>
</ul>
05통통한 플랜 카드
추천 플랜만 한 단 더
추천 플랜 카드만 한 단 더 부풀어 커지고, 누르면 살짝 눌렸다 튀는 동작이 붙어 표 없이도 어떤 플랜을 밀고 있는지 바로 보입니다.

<main class="stage">
<div class="pricing">
<div class="pricing__card pricing__card--basic">
<p class="pricing__name">라이트</p>
<p class="pricing__price">무료</p>
</div>
<div class="pricing__card pricing__card--pro is-demo">
<span class="pricing__badge">추천</span>
<p class="pricing__name">프로</p>
<p class="pricing__price">월 12,000원</p>
<button class="pricing__cta" type="button">선택하기</button>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 눌렸다 튀는 한 주기
$easing: $ease-pop; // 오버슈트 곡선
$color: $subject-blue; // 강조 색
@include stage($stage-ink);
.pricing {
display: flex;
align-items: flex-end;
gap: $sp-4;
width: clamp(240px, 80vw, 360px);
}
.pricing__card {
flex: 1;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: $sp-1;
padding: $sp-5 $sp-4;
border-radius: $r-card;
background: $subject-cream;
}
.pricing__card--basic {
background: rgba($subject-cream, .8);
box-shadow: inset 4px 4px 8px rgba($stage-ink, .22), inset -4px -4px 8px rgba($stage-paper, .18);
}
.pricing__card--pro {
position: relative;
margin-top: $sp-4;
box-shadow: 10px 10px 20px rgba($stage-ink, .35), -8px -8px 18px rgba($stage-paper, .35);
transform-origin: center bottom;
animation: press $duration $easing infinite;
}
.pricing__badge {
position: absolute;
top: -14px;
left: $sp-4;
border-radius: $r-pill;
background: $subject-blue-deep;
color: $subject-cream;
padding: $sp-1 $sp-3;
font-weight: 700; font-size: clamp(9.5px, 2.4vw, 11px); line-height: 1;
}
.pricing__name {
margin: 0;
color: $subject-ink;
font-weight: 700; font-size: clamp(13px, 3vw, 15px); line-height: 1.3;
}
.pricing__price {
margin: 0;
color: rgba($subject-ink, .7);
font-weight: 600; font-size: clamp(11px, 2.6vw, 13px); line-height: 1.4;
}
.pricing__cta {
margin-top: $sp-3;
border: 0;
border-radius: $r-pill;
background: $subject-ink;
color: $subject-cream;
padding: $sp-2 $sp-4;
font-weight: 700; font-size: clamp(11px, 2.6vw, 13px); line-height: 1;
}
// 데모: 추천 카드가 눌렸다 튀어 오르며 정착한다
@keyframes press {
0% { transform: scale(1); }
40% { transform: scale(.94); }
55% { transform: scale(1.06); }
70% { transform: scale(.98); }
100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.pricing__card--pro { 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: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.pricing {
display: flex;
align-items: flex-end;
gap: 16px;
width: clamp(240px, 80vw, 360px);
}
.pricing__card {
flex: 1;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 4px;
padding: 20px 16px;
border-radius: 16px;
background: #fff7e6;
}
.pricing__card--basic {
background: rgba(255, 247, 230, 0.8);
box-shadow: inset 4px 4px 8px rgba(23, 20, 26, 0.22), inset -4px -4px 8px rgba(255, 255, 255, 0.18);
}
.pricing__card--pro {
position: relative;
margin-top: 16px;
box-shadow: 10px 10px 20px rgba(23, 20, 26, 0.35), -8px -8px 18px rgba(255, 255, 255, 0.35);
transform-origin: center bottom;
animation: press 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.pricing__badge {
position: absolute;
top: -14px;
left: 16px;
border-radius: 999px;
background: #0a4fe8;
color: #fff7e6;
padding: 4px 12px;
font-weight: 700;
font-size: clamp(9.5px, 2.4vw, 11px);
line-height: 1;
}
.pricing__name {
margin: 0;
color: #17141a;
font-weight: 700;
font-size: clamp(13px, 3vw, 15px);
line-height: 1.3;
}
.pricing__price {
margin: 0;
color: rgba(23, 20, 26, 0.7);
font-weight: 600;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1.4;
}
.pricing__cta {
margin-top: 12px;
border: 0;
border-radius: 999px;
background: #17141a;
color: #fff7e6;
padding: 8px 16px;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
@keyframes press {
0% {
transform: scale(1);
}
40% {
transform: scale(0.94);
}
55% {
transform: scale(1.06);
}
70% {
transform: scale(0.98);
}
100% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.pricing__card--pro {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./PricingClay.module.scss";
export type PricingClayProps = {
basicName?: string;
basicPrice?: string;
proName?: string;
proPrice?: string;
badge?: string;
cta?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PricingClay({
basicName = "라이트",
basicPrice = "무료",
proName = "프로",
proPrice = "월 12,000원",
badge = "추천",
cta = "선택하기",
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#0a4fe8",
}: PricingClayProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.pricing} style={vars}>
<div className={`${styles.card} ${styles.basic}`}>
<p className={styles.name}>{basicName}</p>
<p className={styles.price}>{basicPrice}</p>
</div>
<div className={`${styles.card} ${styles.pro}`}>
<span className={styles.badge}>{badge}</span>
<p className={styles.name}>{proName}</p>
<p className={styles.price}>{proPrice}</p>
<button type="button" className={styles.cta}>{cta}</button>
</div>
</div>
);
}05 · 통통한 플랜 카드 — 측정 누적면적 10.817% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
배지는 테두리 바깥에
배지는 카드 안쪽이 아니라 카드 테두리 바깥 위에 반쯤 걸쳐 얹어야 아래 글자와 겹치지 않습니다.
<div class="relative flex-[1.1] rounded-clay bg-white p-6 pt-8
shadow-clay-raised">
<span class="absolute -top-3 left-6 rounded-pill bg-accent px-3 py-1
text-xs font-bold text-cream">추천</span>
<p class="font-bold">프로</p>
<p class="mt-1 text-sm text-ink/70">월 12,000원</p>
</div>
06둥근 아바타+말풍선
아바타와 말풍선
아바타가 둥근 캡슐 프레임에 파묻히듯 앉고, 말풍선 카드가 찰흙처럼 통통하게 붙습니다.

<main class="stage">
<div class="testi is-demo">
<span class="testi__avatar" aria-hidden="true"></span>
<div class="testi__bubble">
<p class="testi__quote">"손끝에 감기는 느낌이에요"</p>
<p class="testi__name">오하늘 · 프로덕트 디자이너</p>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 아바타 흔들림·말풍선 뜸의 한 주기
$easing: $ease-spring; // 곡선
$color: $subject-ink; // 강조 색 (노랑 무대라 먹색으로 대비)
@include stage($stage-yellow);
.testi {
display: flex;
align-items: center;
gap: $sp-4;
width: clamp(240px, 80vw, 360px);
}
.testi__avatar {
flex: none;
width: 56px;
height: 56px;
border-radius: $r-pill;
background: $subject-blue;
box-shadow: 6px 6px 12px rgba($stage-ink, .22), -6px -6px 12px rgba($stage-paper, .55);
animation: wobble $duration $easing infinite;
}
.testi__bubble {
flex: 1;
box-sizing: border-box;
border-radius: $r-card;
background: $subject-cream;
padding: $sp-4 $sp-5;
box-shadow: 8px 8px 16px rgba($stage-ink, .22), -8px -8px 16px rgba($stage-paper, .55);
animation: bubbleIn $duration $easing infinite;
animation-delay: -0.4s;
}
.testi__quote {
margin: 0 0 $sp-1;
color: $color;
font-weight: 600; font-size: clamp(12px, 2.8vw, 14px); line-height: 1.4;
}
.testi__name {
margin: 0;
color: rgba($color, .65);
font-weight: 500; font-size: clamp(10.5px, 2.4vw, 12px); line-height: 1.3;
}
// 데모: 아바타가 살짝 좌우로 흔들리고, 말풍선은 뜨고 가라앉는다
@keyframes wobble {
0%, 100% { transform: rotate(0deg); }
50% { transform: rotate(6deg); }
}
@keyframes bubbleIn {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-4px) scale(1.015); }
}
@media (prefers-reduced-motion: reduce) {
.testi__avatar, .testi__bubble { 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: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.testi {
display: flex;
align-items: center;
gap: 16px;
width: clamp(240px, 80vw, 360px);
}
.testi__avatar {
flex: none;
width: 56px;
height: 56px;
border-radius: 999px;
background: #2f6df6;
box-shadow: 6px 6px 12px rgba(23, 20, 26, 0.22), -6px -6px 12px rgba(255, 255, 255, 0.55);
animation: wobble 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.testi__bubble {
flex: 1;
box-sizing: border-box;
border-radius: 16px;
background: #fff7e6;
padding: 16px 20px;
box-shadow: 8px 8px 16px rgba(23, 20, 26, 0.22), -8px -8px 16px rgba(255, 255, 255, 0.55);
animation: bubbleIn 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: -0.4s;
}
.testi__quote {
margin: 0 0 4px;
color: #17141a;
font-weight: 600;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.4;
}
.testi__name {
margin: 0;
color: rgba(23, 20, 26, 0.65);
font-weight: 500;
font-size: clamp(10.5px, 2.4vw, 12px);
line-height: 1.3;
}
@keyframes wobble {
0%, 100% {
transform: rotate(0deg);
}
50% {
transform: rotate(6deg);
}
}
@keyframes bubbleIn {
0%, 100% {
transform: translateY(0) scale(1);
}
50% {
transform: translateY(-4px) scale(1.015);
}
}
@media (prefers-reduced-motion: reduce) {
.testi__avatar, .testi__bubble {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./TestimonialClay.module.scss";
export type TestimonialClayProps = {
quote?: string;
name?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TestimonialClay({
quote = '"손끝에 감기는 느낌이에요"',
name = "오하늘 · 프로덕트 디자이너",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#17141a",
}: TestimonialClayProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.testi} style={vars}>
<span className={styles.avatar} aria-hidden="true" />
<div className={styles.bubble}>
<p className={styles.quote}>{quote}</p>
<p className={styles.name}>{name}</p>
</div>
</div>
);
}06 · 둥근 아바타+말풍선 — 측정 누적면적 7.151% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
시작 시각을 어긋내면
아바타와 말풍선의 움직임 시작 시각을 살짝 어긋내면 서로 다른 물체처럼 자연스럽게 보입니다.
<div class="flex items-center gap-4">
<span class="h-14 w-14 flex-none rounded-pill bg-accent
shadow-clay-raised"></span>
<div class="flex-1 rounded-clay bg-white p-5 shadow-clay-raised">
<p class="text-sm font-semibold">"손끝에 감기는 느낌이에요"</p>
</div>
</div>
07파스텔 CTA 밴드
한 단 떠오른 두꺼운 밴드
가로로 긴 밴드가 배경보다 한 단 떠오른 두꺼운 카드로 앉고, 뒤에 숨은 원형 빛이 천천히 번졌다 가라앉습니다.

<main class="stage">
<div class="ctaband">
<div class="ctaband__glow is-demo" aria-hidden="true"></div>
<p class="ctaband__title">지금 팀에 합류시켜 보세요</p>
<button class="ctaband__cta is-demo" type="button">무료로 시작</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 빛 번짐·버튼 맥동의 한 주기
$easing: $ease-pop; // 오버슈트 곡선
$color: $subject-blue; // 배경 빛 색
@include stage($stage-orange);
.ctaband {
position: relative;
overflow: hidden;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $sp-4;
width: clamp(260px, 86vw, 400px);
border-radius: $r-card;
background: $subject-ink;
padding: $sp-6 $sp-5;
box-shadow: 10px 10px 20px rgba($stage-ink, .3), -6px -6px 16px rgba($stage-paper, .18);
}
.ctaband__glow {
position: absolute;
top: -30%;
right: -10%;
width: 50%;
aspect-ratio: 1;
border-radius: $r-pill;
background: rgba($color, .35);
animation: glowPulse $duration $easing infinite;
}
.ctaband__title {
position: relative;
margin: 0;
max-width: 70%;
color: $subject-cream;
font-weight: 700; font-size: clamp(16px, 4vw, 20px); line-height: 1.3;
}
.ctaband__cta {
position: relative;
border: 0;
border-radius: $r-pill;
background: $subject-cream;
color: $subject-ink;
padding: $sp-2 $sp-6;
font-weight: 700; font-size: clamp(12px, 2.8vw, 14px); line-height: 1;
box-shadow: 4px 4px 8px rgba($stage-ink, .4);
animation: ctaPress $duration $easing infinite;
}
// 데모: 배경 빛이 부풀고, 버튼이 맥동한다
@keyframes glowPulse {
0%, 100% { transform: scale(1); opacity: .35; }
50% { transform: scale(1.15); opacity: .55; }
}
@keyframes ctaPress {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.06); }
}
@media (prefers-reduced-motion: reduce) {
.ctaband__glow, .ctaband__cta { animation: none !important; transform: none; opacity: .35; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ctaband {
position: relative;
overflow: hidden;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
width: clamp(260px, 86vw, 400px);
border-radius: 16px;
background: #17141a;
padding: 24px 20px;
box-shadow: 10px 10px 20px rgba(23, 20, 26, 0.3), -6px -6px 16px rgba(255, 255, 255, 0.18);
}
.ctaband__glow {
position: absolute;
top: -30%;
right: -10%;
width: 50%;
aspect-ratio: 1;
border-radius: 999px;
background: rgba(47, 109, 246, 0.35);
animation: glowPulse 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.ctaband__title {
position: relative;
margin: 0;
max-width: 70%;
color: #fff7e6;
font-weight: 700;
font-size: clamp(16px, 4vw, 20px);
line-height: 1.3;
}
.ctaband__cta {
position: relative;
border: 0;
border-radius: 999px;
background: #fff7e6;
color: #17141a;
padding: 8px 24px;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1;
box-shadow: 4px 4px 8px rgba(23, 20, 26, 0.4);
animation: ctaPress 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes glowPulse {
0%, 100% {
transform: scale(1);
opacity: 0.35;
}
50% {
transform: scale(1.15);
opacity: 0.55;
}
}
@keyframes ctaPress {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.06);
}
}
@media (prefers-reduced-motion: reduce) {
.ctaband__glow, .ctaband__cta {
animation: none !important;
transform: none;
opacity: 0.35;
}
}import type { CSSProperties } from "react";
import styles from "./CtaBandClay.module.scss";
export type CtaBandClayProps = {
title?: string;
cta?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function CtaBandClay({
title = "지금 팀에 합류시켜 보세요",
cta = "무료로 시작",
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#2f6df6",
}: CtaBandClayProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.band} style={vars}>
<div className={styles.glow} aria-hidden="true" />
<p className={styles.title}>{title}</p>
<button type="button" className={styles.cta}>{cta}</button>
</div>
);
}07 · 파스텔 CTA 밴드 — 측정 누적면적 14.983% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
빛이 새지 않게 가둔다
카드에는 overflow: hidden을 걸어 빛이 카드 밖으로 새지 않게 가둡니다.
<div class="relative overflow-hidden rounded-clay bg-ink px-10 py-12
shadow-clay-raised">
<span class="absolute -right-16 -top-24 h-64 w-64 rounded-pill
bg-accent opacity-30 blur-sm"></span>
<h3 class="relative max-w-md text-3xl font-extrabold text-cream">
지금 팀에 합류시켜 보세요
</h3>
</div>
08둥근 상단 푸터
크게 말린 상단 모서리
푸터 상단 모서리가 크게 둥글게 말려 위쪽 콘텐츠를 부드럽게 감싸듯 얹힙니다.

<main class="stage">
<footer class="footerclay">
<div class="footerclay__col footerclay__col--a is-demo">
<p class="footerclay__h">제품</p>
<p class="footerclay__link">기능</p>
</div>
<div class="footerclay__col footerclay__col--b is-demo">
<p class="footerclay__h">회사</p>
<p class="footerclay__link">소개</p>
</div>
<div class="footerclay__col footerclay__col--c is-demo">
<p class="footerclay__h">고객지원</p>
<p class="footerclay__link">도움말</p>
</div>
</footer>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 칸이 뜨고 가라앉는 한 주기
$easing: $ease-out; // 곡선
$color: $subject-blue; // 강조 색
@include stage($stage-paper);
.footerclay {
margin: 0;
box-sizing: border-box;
display: flex;
gap: $sp-3;
width: clamp(240px, 84vw, 380px);
border-radius: $r-card $r-card 0 0;
background: $subject-cream;
padding: $sp-5 $sp-4;
box-shadow: 8px 8px 16px rgba($stage-ink, .16), -8px -8px 16px rgba($stage-paper, .9);
}
.footerclay__col {
flex: 1;
box-sizing: border-box;
border-radius: $r-sm;
background: rgba($stage-ink, .04);
padding: $sp-3;
animation: colFloat $duration $easing infinite;
}
.footerclay__col--a { animation-delay: 0s; }
.footerclay__col--b { animation-delay: -0.6s; }
.footerclay__col--c { animation-delay: -1.2s; }
.footerclay__h {
margin: 0 0 $sp-1;
color: rgba($subject-ink, .72);
font-weight: 700; font-size: clamp(9px, 2.2vw, 10.5px); line-height: 1;
letter-spacing: .04em;
text-transform: uppercase;
}
.footerclay__link {
margin: 0;
color: $subject-ink;
font-weight: 600; font-size: clamp(11px, 2.6vw, 13px); line-height: 1.3;
}
// 데모: 세 칸이 서로 다른 박자로 살짝 떠올랐다 가라앉는다
@keyframes colFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@media (prefers-reduced-motion: reduce) {
.footerclay__col { 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);
}
.footerclay {
margin: 0;
box-sizing: border-box;
display: flex;
gap: 12px;
width: clamp(240px, 84vw, 380px);
border-radius: 16px 16px 0 0;
background: #fff7e6;
padding: 20px 16px;
box-shadow: 8px 8px 16px rgba(23, 20, 26, 0.16), -8px -8px 16px rgba(255, 255, 255, 0.9);
}
.footerclay__col {
flex: 1;
box-sizing: border-box;
border-radius: 8px;
background: rgba(23, 20, 26, 0.04);
padding: 12px;
animation: colFloat 2s ease-out infinite;
}
.footerclay__col--a {
animation-delay: 0s;
}
.footerclay__col--b {
animation-delay: -0.6s;
}
.footerclay__col--c {
animation-delay: -1.2s;
}
.footerclay__h {
margin: 0 0 4px;
color: rgba(23, 20, 26, 0.72);
font-weight: 700;
font-size: clamp(9px, 2.2vw, 10.5px);
line-height: 1;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.footerclay__link {
margin: 0;
color: #17141a;
font-weight: 600;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1.3;
}
@keyframes colFloat {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
@media (prefers-reduced-motion: reduce) {
.footerclay__col {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./FooterClay.module.scss";
export type FooterClayCol = { h: string; link: string };
export type FooterClayProps = {
cols?: FooterClayCol[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const DELAYS = ["0s", "-0.6s", "-1.2s"];
export default function FooterClay({
cols = [
{ h: "제품", link: "기능" },
{ h: "회사", link: "소개" },
{ h: "고객지원", link: "도움말" },
],
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
}: FooterClayProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<footer className={styles.footer} style={vars}>
{cols.map((col, i) => (
<div key={col.h} className={styles.col} style={{ animationDelay: DELAYS[i % DELAYS.length] }}>
<p className={styles.h}>{col.h}</p>
<p className={styles.link}>{col.link}</p>
</div>
))}
</footer>
);
}08 · 둥근 상단 푸터 — 측정 누적면적 1.393% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
구분선 없이 끝까지
링크 묶음 칸마다 옅은 배경만 주고 구분선은 두지 않아, 격자 스타일을 페이지 끝까지 끌고 갑니다.
<footer class="grid grid-cols-3 gap-4 rounded-t-clay bg-white/70 px-4
pt-8 pb-6 shadow-clay-raised">
<div class="rounded-control bg-ink/[.04] p-5">
<p class="text-xs font-bold uppercase text-ink/50">제품</p>
<p class="text-sm font-semibold">기능</p>
</div>
</footer>
09찰흙 버튼·인풋
누르면 그림자가 뒤집힌다
버튼은 누르면 그림자가 안으로 뒤집혀 실제로 눌리는 촉감을 주고, 입력창은 포커스 시 테두리가 밝은 안쪽 글로우로 바뀝니다.

<main class="stage">
<div class="uiclay">
<button class="uiclay__btn is-demo" type="button">저장하기</button>
<label class="uiclay__field is-demo">
<input class="uiclay__input" type="text" placeholder="이름을 입력하세요" readonly>
</label>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 눌림·포커스 전환의 한 주기
$easing: $ease-pop; // 눌림 곡선
$color: $subject-blue; // 포커스 글로우 색
@include stage($stage-ink);
.uiclay {
display: flex;
flex-direction: column;
gap: $sp-4;
width: clamp(220px, 70vw, 300px);
}
.uiclay__btn {
border: 0;
border-radius: $r-control;
background: $subject-blue-deep;
color: $subject-cream;
padding: $sp-3 $sp-5;
font-weight: 700; font-size: clamp(12px, 2.8vw, 14px); line-height: 1;
box-shadow: 6px 6px 12px rgba($stage-ink, .3), -4px -4px 10px rgba($stage-paper, .35);
animation: btnPress $duration $easing infinite;
}
.uiclay__field {
position: relative;
display: block;
border-radius: $r-control;
background: rgba($stage-paper, .06);
padding: $sp-1;
box-shadow: inset 3px 3px 6px rgba($stage-ink, .25), inset -3px -3px 6px rgba($stage-paper, .25);
animation: focusGlow $duration $easing infinite;
}
.uiclay__input {
width: 100%;
box-sizing: border-box;
border: 0;
background: transparent;
border-radius: $r-control;
padding: $sp-3 $sp-4;
color: $subject-cream;
font-weight: 500; font-size: clamp(12px, 2.8vw, 14px); line-height: 1;
outline: none;
}
.uiclay__input::placeholder { color: rgba($subject-cream, .5); }
// 데모: 버튼은 눌리며 그림자가 안으로 뒤집히고, 입력창은 포커스 글로우가 켜졌다 꺼진다
@keyframes btnPress {
0%, 55%, 100% {
box-shadow: 6px 6px 12px rgba($stage-ink, .3), -4px -4px 10px rgba($stage-paper, .35);
transform: scale(1);
}
75% {
box-shadow: inset 4px 4px 8px rgba($stage-ink, .35), inset -4px -4px 8px rgba($stage-paper, .3);
transform: scale(.97);
}
}
@keyframes focusGlow {
0%, 55%, 100% {
box-shadow: inset 3px 3px 6px rgba($stage-ink, .25), inset -3px -3px 6px rgba($stage-paper, .25);
}
75% {
box-shadow: inset 2px 2px 4px rgba($stage-ink, .15), inset -2px -2px 4px rgba($stage-paper, .15), 0 0 0 3px rgba($color, .45);
}
}
@media (prefers-reduced-motion: reduce) {
.uiclay__btn, .uiclay__field { 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: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.uiclay {
display: flex;
flex-direction: column;
gap: 16px;
width: clamp(220px, 70vw, 300px);
}
.uiclay__btn {
border: 0;
border-radius: 12px;
background: #0a4fe8;
color: #fff7e6;
padding: 12px 20px;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1;
box-shadow: 6px 6px 12px rgba(23, 20, 26, 0.3), -4px -4px 10px rgba(255, 255, 255, 0.35);
animation: btnPress 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.uiclay__field {
position: relative;
display: block;
border-radius: 12px;
background: rgba(255, 255, 255, 0.06);
padding: 4px;
box-shadow: inset 3px 3px 6px rgba(23, 20, 26, 0.25), inset -3px -3px 6px rgba(255, 255, 255, 0.25);
animation: focusGlow 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.uiclay__input {
width: 100%;
box-sizing: border-box;
border: 0;
background: transparent;
border-radius: 12px;
padding: 12px 16px;
color: #fff7e6;
font-weight: 500;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1;
outline: none;
}
.uiclay__input::placeholder {
color: rgba(255, 247, 230, 0.5);
}
@keyframes btnPress {
0%, 55%, 100% {
box-shadow: 6px 6px 12px rgba(23, 20, 26, 0.3), -4px -4px 10px rgba(255, 255, 255, 0.35);
transform: scale(1);
}
75% {
box-shadow: inset 4px 4px 8px rgba(23, 20, 26, 0.35), inset -4px -4px 8px rgba(255, 255, 255, 0.3);
transform: scale(0.97);
}
}
@keyframes focusGlow {
0%, 55%, 100% {
box-shadow: inset 3px 3px 6px rgba(23, 20, 26, 0.25), inset -3px -3px 6px rgba(255, 255, 255, 0.25);
}
75% {
box-shadow: inset 2px 2px 4px rgba(23, 20, 26, 0.15), inset -2px -2px 4px rgba(255, 255, 255, 0.15), 0 0 0 3px rgba(47, 109, 246, 0.45);
}
}
@media (prefers-reduced-motion: reduce) {
.uiclay__btn, .uiclay__field {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./ButtonsInputsClay.module.scss";
export type ButtonsInputsClayProps = {
buttonLabel?: string;
placeholder?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ButtonsInputsClay({
buttonLabel = "저장하기",
placeholder = "이름을 입력하세요",
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#0a4fe8",
}: ButtonsInputsClayProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.ui} style={vars}>
<button type="button" className={styles.btn}>{buttonLabel}</button>
<label className={styles.field}>
<input className={styles.input} type="text" placeholder={placeholder} readOnly />
</label>
</div>
);
}09 · 찰흙 버튼·인풋 — 측정 누적면적 8.907% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
카드보다 한 단 작은 반경
두 요소 모두 모서리 반경을 카드보다 한 단 작은 값으로 맞춰 화면 안에서 크기 위계가 느껴지게 합니다.
<form class="flex gap-3 rounded-clay bg-white p-6 shadow-clay-raised">
<label class="relative flex-1 rounded-control bg-cream
shadow-clay-press">
<input class="w-full rounded-control bg-transparent px-4 py-3
text-sm outline-none" type="email"
placeholder="이메일 주소">
</label>
<button class="rounded-control bg-accent px-5 py-3 text-sm font-bold
text-cream shadow-clay-raised">가입하기</button>
</form>
tailwind.config 커스텀
값은 한 곳에만
색·반경·그림자를 이 한 곳에만 적어 두면 원페이지와 부품 아홉 개가 전부 같은 값을 따라갑니다.
이중 그림자에 이름 하나
클레이 특유의 이중 그림자도 매번 새로 쓰지 않고 shadow-clay-raised 하나로 부릅니다.
theme: {
extend: {
colors: {
cream: "#fff7e6",
ink: "#17141a",
accent: "#2f6df6",
},
borderRadius: {
clay: "16px",
control: "12px",
pill: "999px",
},
boxShadow: {
"clay-raised": "8px 8px 16px rgba(23,20,26,.28), -8px -8px 16px rgba(255,255,255,.5)",
"clay-press": "inset 4px 4px 8px rgba(23,20,26,.28), inset -4px -4px 8px rgba(255,255,255,.5)",
},
spacing: {
gutter: "16px",
},
},
},
바이브코딩 프롬프트
짧게 지시만 주면 된다
위 설정을 프로젝트에 붙여 넣은 다음, Claude Code나 Cursor에는 아래처럼 짧게 지시만 주면 됩니다. 프롬프트 전문은 zip 안 prompt.md 파일에 있습니다.
카드·버튼·입력창 모서리는 rounded-clay(16px) 또는 rounded-control(12px)로 통일한다.
그림자는 shadow-clay-raised 하나(어두운 그림자 + 밝은 하이라이트를 함께 그린 것)만 쓰고,
칸마다 진하기를 다르게 만들지 않는다.
눌리는 요소(카드·버튼)에는 shadow-clay-press를 눌림 상태로 준비해 둔다.
배경은 cream 하나로 통일하고, 강조가 필요한 곳에만 accent를 쓴다.
실제로 돌린 화면
이 지시를 page/index.html에 실제로 넣어 돌린 화면을 1280px 너비로 통째로 캡처한 것이 아래 스크린샷입니다.

어디서 깨지는가 — 함정
320px 폭에서 넘친다
가장 먼저 걸린 건 320px 폭이었습니다. 히어로·카드 격자·아이콘 목록 세 부품 모두 블롭 크기와 카드 안쪽 여백을 고정값으로 두었더니 좁은 화면에서 콘텐츠가 정해진 높이를 최대 3px 넘겨 스크롤이 생겼고, 블롭의 위치를 퍼센트로 잡았던 것도 컨테이너 높이가 콘텐츠에 따라 자동으로 정해지는 상황과 맞물려 계산이 꼬였습니다. 블롭 크기와 카드 안쪽 여백을 clamp()로 바꾸고 블롭 위치를 퍼센트 대신 고정 px 오프셋으로 옮기고 나서야 320px에서도 넘치지 않았습니다. 둥근 그림자가 상자 모양으로 되돌아가는 사고는 그림자 표현이 제한적인 트랜잭셔널 이메일 템플릿에서 가장 심하게 드러납니다.
배지가 글자를 가린다
두 번째는 색 대비였습니다. 캡슐 내비게이션의 인디케이터를 먹색으로 칠했더니 마침 먹색인 메뉴 글자와 겹치는 구간에서 글자가 통째로 안 보였는데, 이 비밀번호 y3sdrgr2이 걸린 zip을 받으면 인디케이터를 밝은 크림색으로 바꿔 어떤 글자 위에 있어도 읽히게 고친 버전을 그대로 확인할 수 있습니다.
세 번째 함정은 추천 플랜 카드의 배지 위치입니다. 배지를 카드 안쪽 여백 안에 두면 플랜 이름 글자와 겹쳐 일부 글자가 가려지므로, 배지는 항상 카드 테두리 바깥으로 반쯤 걸치게 띄워야 합니다.
동작 줄이기를 켜면
아홉 개 데모 전부 MDN prefers-reduced-motion 문서가 정의한 미디어 쿼리를 감지하면 애니메이션을 끄고 최종 상태(카드는 떠오른 채, 버튼은 눌리기 전 raised 상태)로 고정합니다. 동작 줄이기를 켠 방문자에게는 배경 블롭이나 눌림 효과가 아예 안 보이는 대신, 카드·버튼·입력창 자체는 그대로 남아 정보 손실이 없습니다.
접근성
작은 글자의 대비
accent(#2f6df6) 배경 위에 얹는 작은 텍스트는 대비가 낮은 편이라, 배지처럼 크기가 작은 글자는 항상 cream 텍스트를 올리고 큰 제목에만 어두운 배경과 밝은 글자 조합을 씁니다.
볼록과 오목을 한 속성으로
카드의 box-shadow는 MDN box-shadow 문서가 안내하는 대로 색상값 뒤에 inset 유무로 볼록/오목을 구분하는데, 두 겹을 동시에 써 볼록한 겉면과 오목한 눌림 상태를 한 속성 안에서 표현했습니다.
더 읽을 거리
벤토 그리드 같은 다른 유행 스타일은 디자인 트렌드 카테고리에서 볼 수 있고, 이 사이트 전체가 뭘 다루는 곳인지는 소개 페이지에 적어 뒀습니다.
FAQ
클레이모피즘 ui와 뉴모피즘은 뭐가 다른가요?
뉴모피즘은 배경과 카드가 거의 같은 색이라 그림자만으로 입체감을 주고, 클레이모피즘은 카드 색을 배경과 분명히 다르게 두고 두꺼운 반경까지 더해 대비와 입체감을 동시에 키웁니다. 그래서 클레이모피즘이 접근성 대비 확보가 상대적으로 더 쉽습니다.
그림자를 두 겹씩 겹치면 느려지지 않나요?
이 편의 아홉 개 데모는 전부 box-shadow 값 자체는 고정해 두고 transform·opacity(09번만 예외로 box-shadow 크로스페이드 추가)만 움직여서, 매 프레임 그림자를 다시 계산하지 않습니다. 카드 수가 아주 많은 화면에서는 그림자 두 겹이 페인트 비용을 조금 늘리므로, 스크롤에 걸리는 카드에는 애니메이션을 넣지 않는 편이 안전합니다.
다크 모드에서도 클레이모피즘이 어울리나요?
어울립니다. 다만 밝은 하이라이트와 어두운 그림자의 진하기를 라이트 모드와 반대로 조정해야 합니다. 어두운 배경 위에서는 밝은 하이라이트의 불투명도를 낮추고 어두운 그림자의 불투명도를 높여야 카드가 배경에 파묻히지 않습니다. 통통한 입체 대신 오려 붙인 평면 질감을 원한다면 종이 질감 랜딩 키트가 같은 부품 구성을 다른 재료로 보여 줍니다.









