手書き風 デザイン LPキット、クレヨン風セクション9選
手書き風 デザインは、カードやボタンの周りに太くよれた輪郭線を描き、その輪郭線だけにSVGフィルターをかけて、定規で引いたのではなく手で描いたように見せるスタイルです。手で描いたと思ったら実は CSS だった、というのがこのキットの正体です。
子ども向けの絵画教室やハンドメイドブランドのサイトで見かけるあの雰囲気で、名前が分からず検索できなかった画面そのものです。下にそのまま動く原寸大の1ページLPを置きました。まずナビからフッターまで通しでスクロールしてみてください。

ここからは同じ画面を9つの部品にばらして並べ直しました。訪問者が最初にLPを開いたとき視線が流れる順 — 概観、機能確認、料金、信頼、行動喚起、登録 — そのままです。落書きヒーロー・二重線カード・ジグザグフッターの3つは同じ仕組みで動いています。輪郭線を描くSVG図形だけに揺れフィルターをかけ、文字を包む要素には一切かけません。
01 hero-crayon
02 nav-crayon
03 card-grid-crayon
04 feature-list-crayon
05 pricing-crayon
06 testimonial-crayon
07 cta-band-crayon
08 footer-crayon
09 buttons-inputs-crayon
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 クレヨン落書きヒーロー
- 02 手書き丸ナビ
- 03 よれよれステッカーカードグリッド
- 04 手書きチェックリスト
- 05 星と丸の料金プラン
- 06 クレヨン吹き出しのレビュー
- 07 クレヨンハッチングCTA帯
- 08 はさみ切りジグザグフッター
- 09 二重線クレヨンボタン・入力欄
このキット全体の色は、パワポ 手書き風 デザインより少ない7つで完結します。文字は常に一番下のインク色1つに固定するのが、読める手書き風デザインの条件です。同じdesign柱で色をキャンディのように明るく使う側がパステル デザイン LPキット、果物の色で清涼感へ持ち上げた側がビタミンカラー デザイン LPキットです。
| 役割 | 色 | 使う場所 |
|---|---|---|
| 紙 | #fff8ef |
ページ背景全体 |
| インク | #33261c |
すべての文字(コントラスト12.1〜13.9:1) |
| レッドクレヨン | #ff5a5f |
手描きの線・強調枠 |
| イエロークレヨン | #ffcd3c |
ボタン・星・下線 |
| ブルークレヨン | #4fa3e3 |
二重線・フォーカスリング |
| グリーンクレヨン | #6dbe6d |
チェック・アバター |
| パープルクレヨン | #a685e2 |
CTA背景の淡色 |
01クレヨン落書きヒーロー
クレヨン デザイン 文字で何が見えるか
カードの枠線は文字とは別のSVGレイヤーで、そのレイヤーだけにfeTurbulenceの揺れフィルターをかけているので、太い点線が手描きのように波打ちます。

<main class="stage">
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<filter id="rough1" x="-30%" y="-30%" width="160%" height="160%">
<feTurbulence type="fractalNoise" baseFrequency="0.045 0.06" numOctaves="2" seed="4" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="7" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<section class="herocrayon">
<svg class="herocrayon__frame" viewBox="0 0 300 190" preserveAspectRatio="none" aria-hidden="true">
<rect x="6" y="6" width="288" height="178" rx="20" class="herocrayon__frame-rect"/>
</svg>
<div class="herocrayon__card">
<p class="herocrayon__eyebrow">CRAYON KIDS</p>
<h1 class="herocrayon__title">手で描いたみたいに</h1>
<svg class="herocrayon__underline" viewBox="0 0 170 16" aria-hidden="true">
<path class="herocrayon__underline-path" d="M4 9 C 28 3, 54 14, 84 7 S 136 3, 166 10" />
</svg>
<p class="herocrayon__desc">クレヨン感性のランディングキット</p>
<button class="herocrayon__cta" type="button">無料体験</button>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 테두리가 한 바퀴 다시 그려지는 주기
$easing: $ease-spring; // 카드가 들썩이는 곡선
$color: $stage-ink; // 글자 색 기본값(토큰) — 아래 크레용 잉크로 덮는다
// 크레용 팔레트 — 이 편의 역할 색. 값은 tailwind.config.js · tokens.css 와 같다
$crayon-paper: #fff8ef; // 종이 바탕
$crayon-ink: #33261c; // 크레용 잉크 — 글자는 항상 이 색(대비 12:1 이상)
$crayon-red: $stage-orange; // 레드 크레용 — 세련 패치: 토큰 정렬(#dbb6b7→#c89a8f, 휘도 0.29→0.27)
$crayon-yellow: $stage-yellow; // 옐로 크레용 — 세련 패치: 토큰 정렬(#d1c5a2→#d2c7a4, 휘도 0.65→0.68)
$crayon-blue: #9cb8ce; // 블루 크레용
$crayon-green: #6dbe6d; // 그린 크레용
$crayon-purple: #cbbfe0; // 퍼플 크레용
@include stage($crayon-paper);
.herocrayon {
position: relative;
width: clamp(230px, 76vw, 320px);
animation: cardBob $duration $easing infinite;
}
// 테두리는 글자와 분리된 SVG 레이어에만 흔들림 필터를 건다 — 글자에 걸면 읽기 어려워진다(접근성 섹션 참고)
.herocrayon__frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.herocrayon__frame-rect {
fill: none;
stroke: $crayon-ink;
stroke-width: 5;
stroke-linecap: round;
stroke-dasharray: 16 11;
filter: url(#rough1);
animation: frameMarch $duration linear infinite;
}
.herocrayon__card {
position: relative;
box-sizing: border-box;
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $sp-2;
padding: $sp-6 $sp-5;
}
.herocrayon__eyebrow {
margin: 0;
color: $subject-orange-deep;
font-weight: 700;
font-size: clamp(10px, 2.6vw, 12px);
line-height: 1;
letter-spacing: .08em;
}
.herocrayon__title {
margin: 0;
color: $crayon-ink;
font-weight: 700;
font-size: clamp(19px, 4.8vw, 25px);
line-height: 1.2;
}
.herocrayon__underline {
width: clamp(120px, 42vw, 170px);
height: 16px;
overflow: visible;
}
.herocrayon__underline-path {
fill: none;
stroke: $crayon-yellow;
stroke-width: 5;
stroke-linecap: round;
stroke-dasharray: 210;
animation: underlineDraw $duration $ease-out infinite;
}
.herocrayon__desc {
margin: 0;
color: rgba($crayon-ink, .72);
font-weight: 500;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.4;
}
.herocrayon__cta {
margin-top: $sp-1;
border: 3px solid $crayon-ink;
border-radius: $r-pill;
background: $crayon-yellow;
color: $crayon-ink;
padding: $sp-2 $sp-6;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1;
}
.herocrayon__cta:focus-visible {
outline: 3px solid $crayon-ink;
outline-offset: 2px;
}
// 데모: 테두리가 크레용으로 계속 덧그리듯 행진하고, 밑줄이 그려졌다 지워지길 반복하며, 카드는 살짝 들썩인다
@keyframes frameMarch {
to { stroke-dashoffset: -540; }
}
@keyframes underlineDraw {
0% { stroke-dashoffset: 210; }
45% { stroke-dashoffset: 0; }
80% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -210; }
}
@keyframes cardBob {
0%, 100% { transform: translateY(0) rotate(-0.4deg); }
50% { transform: translateY(-6px) rotate(0.5deg); }
}
// 반응형: 320×200 처럼 세로가 짧은 화면에선 카드 속을 줄인다 (BD9)
@media (max-width: 340px), (max-height: 220px) {
.herocrayon__card { padding: $sp-4 $sp-4; gap: $sp-1; }
.herocrayon__eyebrow { display: none; }
.herocrayon__underline { width: clamp(90px, 32vw, 120px); }
}
@media (prefers-reduced-motion: reduce) {
.herocrayon,
.herocrayon__frame-rect,
.herocrayon__underline-path {
animation: none !important;
transform: none;
}
.herocrayon__underline-path { 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: #fff8ef;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.herocrayon {
position: relative;
width: clamp(230px, 76vw, 320px);
animation: cardBob 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.herocrayon__frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.herocrayon__frame-rect {
fill: none;
stroke: #33261c;
stroke-width: 5;
stroke-linecap: round;
stroke-dasharray: 16 11;
filter: url(#rough1);
animation: frameMarch 2s linear infinite;
}
.herocrayon__card {
position: relative;
box-sizing: border-box;
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 24px 20px;
}
.herocrayon__eyebrow {
margin: 0;
color: #c93000;
font-weight: 700;
font-size: clamp(10px, 2.6vw, 12px);
line-height: 1;
letter-spacing: 0.08em;
}
.herocrayon__title {
margin: 0;
color: #33261c;
font-weight: 700;
font-size: clamp(19px, 4.8vw, 25px);
line-height: 1.2;
}
.herocrayon__underline {
width: clamp(120px, 42vw, 170px);
height: 16px;
overflow: visible;
}
.herocrayon__underline-path {
fill: none;
stroke: #ffd23f;
stroke-width: 5;
stroke-linecap: round;
stroke-dasharray: 210;
animation: underlineDraw 2s ease-out infinite;
}
.herocrayon__desc {
margin: 0;
color: rgba(51, 38, 28, 0.72);
font-weight: 500;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1.4;
}
.herocrayon__cta {
margin-top: 4px;
border: 3px solid #33261c;
border-radius: 999px;
background: #ffd23f;
color: #33261c;
padding: 8px 24px;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 14px);
line-height: 1;
}
.herocrayon__cta:focus-visible {
outline: 3px solid #33261c;
outline-offset: 2px;
}
@keyframes frameMarch {
to {
stroke-dashoffset: -540;
}
}
@keyframes underlineDraw {
0% {
stroke-dashoffset: 210;
}
45% {
stroke-dashoffset: 0;
}
80% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -210;
}
}
@keyframes cardBob {
0%, 100% {
transform: translateY(0) rotate(-0.4deg);
}
50% {
transform: translateY(-6px) rotate(0.5deg);
}
}
@media (max-width: 340px), (max-height: 220px) {
.herocrayon__card {
padding: 16px 16px;
gap: 4px;
}
.herocrayon__eyebrow {
display: none;
}
.herocrayon__underline {
width: clamp(90px, 32vw, 120px);
}
}
@media (prefers-reduced-motion: reduce) {
.herocrayon,
.herocrayon__frame-rect,
.herocrayon__underline-path {
animation: none !important;
transform: none;
}
.herocrayon__underline-path {
stroke-dashoffset: 0;
}
}import type { CSSProperties } from "react";
import styles from "./HeroCrayon.module.scss";
export type HeroCrayonProps = {
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 HeroCrayon({
eyebrow = "CRAYON KIDS",
title = "손으로 그린 것처럼",
desc = "크레용 감성 랜딩 키트",
cta = "무료 체험",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ff5a5f",
}: HeroCrayonProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.hero} style={vars}>
<svg width="0" height="0" style={{ position: "absolute" }} aria-hidden="true" focusable="false">
<filter id="hc-rough" x="-30%" y="-30%" width="160%" height="160%">
<feTurbulence type="fractalNoise" baseFrequency={0.045} numOctaves={2} seed={4} result="n" />
<feDisplacementMap in="SourceGraphic" in2="n" scale={7} xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>
<svg className={styles.frame} viewBox="0 0 300 190" preserveAspectRatio="none" aria-hidden="true">
<rect x={6} y={6} width={288} height={178} rx={20} className={styles.frameRect} />
</svg>
<div className={styles.card}>
<p className={styles.eyebrow}>{eyebrow}</p>
<h1 className={styles.title}>{title}</h1>
<svg className={styles.underline} viewBox="0 0 170 16" aria-hidden="true">
<path className={styles.underlinePath} d="M4 9 C 28 3, 54 14, 84 7 S 136 3, 166 10" />
</svg>
<p className={styles.desc}>{desc}</p>
<button type="button" className={styles.cta}>{cta}</button>
</div>
</section>
);
}01 · クレヨン落書きヒーロー — 計測 累積面積 16.178% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードのどこを見るか
見出し下の黄色い下線はstroke-dashoffsetで描かれては消えを繰り返します。
<svg width="0" height="0" style="position:absolute">
<filter id="rough" x="-30%" y="-30%" width="160%" height="160%">
<feTurbulence type="fractalNoise" baseFrequency="0.045" numOctaves="2" seed="4" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="7"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<svg class="frame" viewBox="0 0 300 190" preserveAspectRatio="none">
<rect x="6" y="6" width="288" height="178" rx="20"
stroke="#33261c" stroke-width="5" stroke-dasharray="16 11"
fill="none" filter="url(#rough)" />
</svg>
02手書き丸ナビ
画面で起きること
アクティブなタブの文字を赤いクレヨンの丸が囲みますが、きれいな円ではなく片端がもう片端を追い越す二重ループで、たった今描いたように見えます。

<main class="stage">
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<filter id="rough2" x="-40%" y="-40%" width="180%" height="180%">
<feTurbulence type="fractalNoise" baseFrequency="0.05 0.08" numOctaves="2" seed="9" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="5" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<nav class="navcrayon">
<span class="navcrayon__brand">クレヨン</span>
<ul class="navcrayon__list">
<li class="navcrayon__item navcrayon__item--active">
<span class="navcrayon__label">ホーム</span>
<svg class="navcrayon__loop" viewBox="0 0 56 30" aria-hidden="true">
<path class="navcrayon__loop-path" d="M28 4 C 13 2, 4 11, 7 17 C 9 25, 20 28, 29 26 C 41 24, 50 17, 47 10 C 45 4, 33 2, 25 5" />
</svg>
</li>
<li class="navcrayon__item"><span class="navcrayon__label">クラス</span></li>
<li class="navcrayon__item"><span class="navcrayon__label">料金プラン</span></li>
<li class="navcrayon__item"><span class="navcrayon__label">お問い合わせ</span></li>
</ul>
<button class="navcrayon__cta" type="button">体験申し込み</button>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 동그라미가 한 번 그려지는 주기
$easing: $ease-out; // 선이 그려지는 곡선
$color: $stage-ink; // 글자 색 기본값(토큰) — 아래 크레용 잉크로 덮는다
$crayon-paper: #fff8ef;
$crayon-ink: #33261c;
$crayon-red: #dbb6b7;
$crayon-yellow: #d1c5a2;
$crayon-blue: #9cb8ce;
$crayon-green: #6dbe6d;
$crayon-purple: #cbbfe0;
@include stage($crayon-paper);
.navcrayon {
display: flex;
align-items: center;
gap: $sp-4;
width: clamp(260px, 88vw, 380px);
padding: $sp-2 $sp-3;
border-radius: $r-pill;
background: $stage-paper;
border: 3px solid $crayon-ink;
}
.navcrayon__brand {
flex-shrink: 0;
color: $crayon-ink;
font-weight: 700;
font-size: clamp(12px, 3vw, 14px);
padding-left: $sp-2;
}
.navcrayon__list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
align-items: center;
gap: $sp-3;
min-width: 0;
flex: 1;
}
.navcrayon__item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: $sp-1 $sp-2;
}
.navcrayon__label {
position: relative;
z-index: 1;
color: rgba($crayon-ink, .72);
font-weight: 600;
font-size: clamp(10px, 2.6vw, 12px);
white-space: nowrap;
}
.navcrayon__item--active .navcrayon__label {
color: $crayon-ink;
}
.navcrayon__loop {
position: absolute;
left: 50%;
top: 50%;
width: 58px;
height: 32px;
transform: translate(-50%, -50%);
overflow: visible;
pointer-events: none;
filter: url(#rough2);
}
.navcrayon__loop-path {
fill: none;
stroke: $crayon-red;
stroke-width: 3.5;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 110;
animation: loopDraw $duration $easing infinite;
}
.navcrayon__cta {
flex-shrink: 0;
border: 0;
border-radius: $r-pill;
background: $crayon-yellow;
color: $crayon-ink;
padding: $sp-2 $sp-4;
font-weight: 700;
font-size: clamp(10px, 2.6vw, 12px);
white-space: nowrap;
}
.navcrayon__cta:focus-visible {
outline: 3px solid $crayon-ink;
outline-offset: 2px;
}
// 데모: 활성 탭 둘레를 크레용으로 감싸는 동그라미가 그려졌다 지워지길 반복한다
@keyframes loopDraw {
0% { stroke-dashoffset: 110; opacity: 0; }
8% { opacity: 1; }
55% { stroke-dashoffset: 0; opacity: 1; }
85% { stroke-dashoffset: 0; opacity: 1; }
100% { stroke-dashoffset: -110; opacity: 0; }
}
@media (max-width: 340px), (max-height: 220px) {
.navcrayon { gap: $sp-2; padding: $sp-1 $sp-2; }
.navcrayon__brand { display: none; }
.navcrayon__list { gap: $sp-1; }
.navcrayon__cta { padding: $sp-1 $sp-3; }
}
@media (prefers-reduced-motion: reduce) {
.navcrayon__loop-path {
animation: none !important;
stroke-dashoffset: 0;
opacity: 1;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fff8ef;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.navcrayon {
display: flex;
align-items: center;
gap: 16px;
width: clamp(260px, 88vw, 380px);
padding: 8px 12px;
border-radius: 999px;
background: #ffffff;
border: 3px solid #33261c;
}
.navcrayon__brand {
flex-shrink: 0;
color: #33261c;
font-weight: 700;
font-size: clamp(12px, 3vw, 14px);
padding-left: 8px;
}
.navcrayon__list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
flex: 1;
}
.navcrayon__item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 4px 8px;
}
.navcrayon__label {
position: relative;
z-index: 1;
color: rgba(51, 38, 28, 0.72);
font-weight: 600;
font-size: clamp(10px, 2.6vw, 12px);
white-space: nowrap;
}
.navcrayon__item--active .navcrayon__label {
color: #33261c;
}
.navcrayon__loop {
position: absolute;
left: 50%;
top: 50%;
width: 58px;
height: 32px;
transform: translate(-50%, -50%);
overflow: visible;
pointer-events: none;
filter: url(#rough2);
}
.navcrayon__loop-path {
fill: none;
stroke: #dbb6b7;
stroke-width: 3.5;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 110;
animation: loopDraw 2s ease-out infinite;
}
.navcrayon__cta {
flex-shrink: 0;
border: 0;
border-radius: 999px;
background: #d1c5a2;
color: #33261c;
padding: 8px 16px;
font-weight: 700;
font-size: clamp(10px, 2.6vw, 12px);
white-space: nowrap;
}
.navcrayon__cta:focus-visible {
outline: 3px solid #33261c;
outline-offset: 2px;
}
@keyframes loopDraw {
0% {
stroke-dashoffset: 110;
opacity: 0;
}
8% {
opacity: 1;
}
55% {
stroke-dashoffset: 0;
opacity: 1;
}
85% {
stroke-dashoffset: 0;
opacity: 1;
}
100% {
stroke-dashoffset: -110;
opacity: 0;
}
}
@media (max-width: 340px), (max-height: 220px) {
.navcrayon {
gap: 8px;
padding: 4px 8px;
}
.navcrayon__brand {
display: none;
}
.navcrayon__list {
gap: 4px;
}
.navcrayon__cta {
padding: 4px 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.navcrayon__loop-path {
animation: none !important;
stroke-dashoffset: 0;
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./NavCrayon.module.scss";
export type NavCrayonProps = {
brand?: string;
tabs?: string[];
active?: number;
cta?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function NavCrayon({
brand = "크레용",
tabs = ["홈", "클래스", "요금제", "문의"],
active = 0,
cta = "체험 신청",
duration = 2000,
easing = "ease-out",
color = "#ff5a5f",
}: NavCrayonProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<nav className={styles.nav} style={vars}>
<svg width="0" height="0" style={{ position: "absolute" }} aria-hidden="true" focusable="false">
<filter id="nc-rough" x="-40%" y="-40%" width="180%" height="180%">
<feTurbulence type="fractalNoise" baseFrequency={0.06} numOctaves={2} seed={9} result="n" />
<feDisplacementMap in="SourceGraphic" in2="n" scale={5} xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>
<span className={styles.brand}>{brand}</span>
<ul className={styles.list}>
{tabs.map((t, i) => (
<li key={t} className={styles.item}>
<span className={i === active ? `${styles.label} ${styles.labelActive}` : styles.label}>{t}</span>
{i === active && (
<svg className={styles.loop} viewBox="0 0 56 30" aria-hidden="true">
<path className={styles.loopPath} d="M28 4 C 13 2, 4 11, 7 17 C 9 25, 20 28, 29 26 C 41 24, 50 17, 47 10 C 45 4, 33 2, 25 5" />
</svg>
)}
</li>
))}
</ul>
<button type="button" className={styles.cta}>{cta}</button>
</nav>
);
}02 · 手書き丸ナビ — 計測 累積面積 0.344% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードの読み方
stroke-dasharrayで描き切ってから逆方向に消します。
<li class="relative px-2 py-1">
<span class="relative z-10 text-xs font-semibold">ホーム</span>
<svg class="loop" viewBox="0 0 56 30">
<path fill="none" stroke="#ff5a5f" stroke-width="3.5"
stroke-linecap="round" stroke-linejoin="round"
stroke-dasharray="110"
d="M28 4 C 13 2, 4 11, 7 17 C 9 25, 20 28, 29 26
C 41 24, 50 17, 47 10 C 45 4, 33 2, 25 5" />
</svg>
</li>
03よれよれステッカーカードグリッド
どんな動きか
カードごとに赤と青の線を1〜2pxずらして重ね、クレヨンで二度なぞった跡を出します。

<main class="stage">
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<filter id="rough3" x="-30%" y="-30%" width="160%" height="160%">
<feTurbulence type="fractalNoise" baseFrequency="0.05 0.07" numOctaves="2" seed="12" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="5" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<div class="cardcrayon">
<article class="cardcrayon__card cardcrayon__card--1">
<svg class="cardcrayon__frame" viewBox="0 0 100 78" preserveAspectRatio="none" aria-hidden="true">
<rect x="5" y="5" width="90" height="68" rx="12" class="cardcrayon__frame-rect cardcrayon__frame-rect--a" />
<rect x="3" y="3" width="90" height="68" rx="12" class="cardcrayon__frame-rect cardcrayon__frame-rect--b" />
</svg>
<span class="cardcrayon__badge">NEW</span>
<span class="cardcrayon__dot"></span>
<p class="cardcrayon__label">手描きアイコン</p>
</article>
<article class="cardcrayon__card cardcrayon__card--2">
<svg class="cardcrayon__frame" viewBox="0 0 100 78" preserveAspectRatio="none" aria-hidden="true">
<rect x="5" y="5" width="90" height="68" rx="12" class="cardcrayon__frame-rect cardcrayon__frame-rect--a" />
<rect x="3" y="3" width="90" height="68" rx="12" class="cardcrayon__frame-rect cardcrayon__frame-rect--b" />
</svg>
<span class="cardcrayon__badge">人気</span>
<span class="cardcrayon__dot"></span>
<p class="cardcrayon__label">よれよれカード</p>
</article>
<article class="cardcrayon__card cardcrayon__card--3">
<svg class="cardcrayon__frame" viewBox="0 0 100 78" preserveAspectRatio="none" aria-hidden="true">
<rect x="5" y="5" width="90" height="68" rx="12" class="cardcrayon__frame-rect cardcrayon__frame-rect--a" />
<rect x="3" y="3" width="90" height="68" rx="12" class="cardcrayon__frame-rect cardcrayon__frame-rect--b" />
</svg>
<span class="cardcrayon__badge">新着</span>
<span class="cardcrayon__dot"></span>
<p class="cardcrayon__label">クレヨンバッジ</p>
</article>
<article class="cardcrayon__card cardcrayon__card--4">
<svg class="cardcrayon__frame" viewBox="0 0 100 78" preserveAspectRatio="none" aria-hidden="true">
<rect x="5" y="5" width="90" height="68" rx="12" class="cardcrayon__frame-rect cardcrayon__frame-rect--a" />
<rect x="3" y="3" width="90" height="68" rx="12" class="cardcrayon__frame-rect cardcrayon__frame-rect--b" />
</svg>
<span class="cardcrayon__badge">無料</span>
<span class="cardcrayon__dot"></span>
<p class="cardcrayon__label">落書きアニメーション</p>
</article>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 카드가 한 번 기우는 주기
$easing: $ease-spring; // 기우는 곡선
$color: $stage-ink; // 글자 색 기본값(토큰) — 아래 크레용 잉크로 덮는다
$crayon-paper: #fff8ef;
$crayon-ink: #33261c;
$crayon-red: #dbb6b7;
$crayon-yellow: #d1c5a2;
$crayon-blue: #9cb8ce;
$crayon-green: #6dbe6d;
$crayon-purple: #cbbfe0;
$crayon-purple-tint: #efe6fb;
$crayon-yellow-tint: #fff3cc;
$crayon-blue-tint: #dff0fc;
$crayon-green-tint: #e2f5e2;
@include stage($crayon-paper);
.cardcrayon {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: $sp-4;
width: clamp(220px, 80vw, 320px);
}
.cardcrayon__card {
position: relative;
box-sizing: border-box;
min-height: 88px;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: $sp-1;
padding: $sp-3;
border-radius: $r-card;
animation: cardTilt $duration $easing infinite;
}
.cardcrayon__card--1 { background: $crayon-purple-tint; animation-delay: 0ms; }
.cardcrayon__card--2 { background: $crayon-yellow-tint; animation-delay: 140ms; }
.cardcrayon__card--3 { background: $crayon-blue-tint; animation-delay: 280ms; }
.cardcrayon__card--4 { background: $crayon-green-tint; animation-delay: 420ms; }
// 테두리는 글자를 감싸지 않는 별도 SVG 레이어 — 빨강·파랑 두 선을 살짝 어긋나게 겹쳐 크레용 두 번 그은 느낌을 낸다
.cardcrayon__frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
filter: url(#rough3);
}
.cardcrayon__frame-rect {
fill: none;
stroke-width: 2.4;
}
.cardcrayon__frame-rect--a { stroke: $crayon-red; }
.cardcrayon__frame-rect--b { stroke: $crayon-blue; opacity: .8; }
.cardcrayon__badge {
position: absolute;
top: -10px;
right: $sp-3;
border: 2px dashed $crayon-ink;
border-radius: $r-pill;
background: $stage-paper;
color: $crayon-ink;
padding: $sp-1 $sp-2;
font-weight: 700;
font-size: 9px;
line-height: 1.4;
}
.cardcrayon__dot {
width: 30px;
height: 30px;
border-radius: 50%;
background: radial-gradient(circle at 35% 32%, rgba(255, 255, 255, .95), rgba(255, 255, 255, 0) 70%), $crayon-ink;
}
.cardcrayon__label {
margin: 0;
color: $crayon-ink;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1.3;
}
// 데모: 카드 네 장이 순서대로 살짝씩 기운다
@keyframes cardTilt {
0%, 100% { transform: rotate(0deg) translateY(0); }
50% { transform: rotate(-2deg) translateY(-3px); }
}
@media (max-width: 340px), (max-height: 220px) {
.cardcrayon { gap: $sp-2; }
.cardcrayon__card { min-height: 68px; padding: $sp-2; }
.cardcrayon__dot { width: 22px; height: 22px; }
}
@media (prefers-reduced-motion: reduce) {
.cardcrayon__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: #fff8ef;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cardcrayon {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
width: clamp(220px, 80vw, 320px);
}
.cardcrayon__card {
position: relative;
box-sizing: border-box;
min-height: 88px;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 4px;
padding: 12px;
border-radius: 16px;
animation: cardTilt 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.cardcrayon__card--1 {
background: #efe6fb;
animation-delay: 0ms;
}
.cardcrayon__card--2 {
background: #fff3cc;
animation-delay: 140ms;
}
.cardcrayon__card--3 {
background: #dff0fc;
animation-delay: 280ms;
}
.cardcrayon__card--4 {
background: #e2f5e2;
animation-delay: 420ms;
}
.cardcrayon__frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
filter: url(#rough3);
}
.cardcrayon__frame-rect {
fill: none;
stroke-width: 2.4;
}
.cardcrayon__frame-rect--a {
stroke: #dbb6b7;
}
.cardcrayon__frame-rect--b {
stroke: #9cb8ce;
opacity: 0.8;
}
.cardcrayon__badge {
position: absolute;
top: -10px;
right: 12px;
border: 2px dashed #33261c;
border-radius: 999px;
background: #ffffff;
color: #33261c;
padding: 4px 8px;
font-weight: 700;
font-size: 9px;
line-height: 1.4;
}
.cardcrayon__dot {
width: 30px;
height: 30px;
border-radius: 50%;
background: radial-gradient(circle at 35% 32%, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0) 70%), #33261c;
}
.cardcrayon__label {
margin: 0;
color: #33261c;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1.3;
}
@keyframes cardTilt {
0%, 100% {
transform: rotate(0deg) translateY(0);
}
50% {
transform: rotate(-2deg) translateY(-3px);
}
}
@media (max-width: 340px), (max-height: 220px) {
.cardcrayon {
gap: 8px;
}
.cardcrayon__card {
min-height: 68px;
padding: 8px;
}
.cardcrayon__dot {
width: 22px;
height: 22px;
}
}
@media (prefers-reduced-motion: reduce) {
.cardcrayon__card {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./CardGridCrayon.module.scss";
export type CardGridCrayonItem = { label: string; badge: string; tint: "purple" | "yellow" | "blue" | "green" };
export type CardGridCrayonProps = {
items?: CardGridCrayonItem[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const DEFAULT_ITEMS: CardGridCrayonItem[] = [
{ label: "손그림 아이콘", badge: "NEW", tint: "purple" },
{ label: "삐뚤빼뚤 카드", badge: "인기", tint: "yellow" },
{ label: "크레용 배지", badge: "신규", tint: "blue" },
{ label: "낙서 애니메이션", badge: "무료", tint: "green" },
];
export default function CardGridCrayon({
items = DEFAULT_ITEMS,
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ff5a5f",
}: CardGridCrayonProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.grid} style={vars}>
<svg width="0" height="0" style={{ position: "absolute" }} aria-hidden="true" focusable="false">
<filter id="cg-rough" x="-30%" y="-30%" width="160%" height="160%">
<feTurbulence type="fractalNoise" baseFrequency={0.06} numOctaves={2} seed={12} result="n" />
<feDisplacementMap in="SourceGraphic" in2="n" scale={5} xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>
{items.map((it, i) => (
<article key={it.label} className={`${styles.card} ${styles[`tint_${it.tint}`]}`} style={{ animationDelay: `${i * 140}ms` }}>
<svg className={styles.frame} viewBox="0 0 100 78" preserveAspectRatio="none" aria-hidden="true">
<rect x={5} y={5} width={90} height={68} rx={12} className={`${styles.frameRect} ${styles.frameRectA}`} />
<rect x={3} y={3} width={90} height={68} rx={12} className={`${styles.frameRect} ${styles.frameRectB}`} />
</svg>
<span className={styles.badge}>{it.badge}</span>
<span className={styles.dot} />
<p className={styles.label}>{it.label}</p>
</article>
))}
</div>
);
}03 · よれよれステッカーカードグリッド — 計測 累積面積 13.402% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
どこを変えるか
4枚のカードは開始時刻を140msずつずらし、順番に少しずつ傾きます。
<svg class="frame" viewBox="0 0 100 78" preserveAspectRatio="none">
<rect x="5" y="5" width="90" height="68" rx="12"
fill="none" stroke="#ff5a5f" stroke-width="2.4" />
<rect x="3" y="3" width="90" height="68" rx="12"
fill="none" stroke="#4fa3e3" stroke-width="2.4" opacity=".8" />
</svg>
04手書きチェックリスト
何が見えるか
チェックマークはアイコンフォントではなく、1本のSVGストロークが左から右へ描かれるアニメーションです。

<main class="stage">
<ul class="listcrayon">
<li class="listcrayon__row listcrayon__row--1">
<svg class="listcrayon__check" viewBox="0 0 20 16" aria-hidden="true">
<path class="listcrayon__check-path" d="M2 9 C 3 8, 4 10, 7 13 C 10 10, 14 6, 18 2" />
</svg>
<span class="listcrayon__text">ドラッグで配置</span>
</li>
<li class="listcrayon__row listcrayon__row--2">
<svg class="listcrayon__check" viewBox="0 0 20 16" aria-hidden="true">
<path class="listcrayon__check-path" d="M2 9 C 3 8, 4 10, 7 13 C 10 10, 14 6, 18 2" />
</svg>
<span class="listcrayon__text">色を選んで塗る</span>
</li>
<li class="listcrayon__row listcrayon__row--3">
<svg class="listcrayon__check" viewBox="0 0 20 16" aria-hidden="true">
<path class="listcrayon__check-path" d="M2 9 C 3 8, 4 10, 7 13 C 10 10, 14 6, 18 2" />
</svg>
<span class="listcrayon__text">手描きアイコンを選ぶ</span>
</li>
<li class="listcrayon__row listcrayon__row--4">
<svg class="listcrayon__check" viewBox="0 0 20 16" aria-hidden="true">
<path class="listcrayon__check-path" d="M2 9 C 3 8, 4 10, 7 13 C 10 10, 14 6, 18 2" />
</svg>
<span class="listcrayon__text">すぐに共有</span>
</li>
</ul>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 체크가 한 번 그려지는 주기
$easing: $ease-out; // 선이 그려지는 곡선
$color: $stage-ink; // 글자 색 기본값(토큰) — 아래 크레용 잉크로 덮는다
$crayon-paper: #fff8ef;
$crayon-ink: #33261c;
$crayon-red: #dbb6b7;
$crayon-yellow: #d1c5a2;
$crayon-blue: #9cb8ce;
$crayon-green: #6dbe6d;
$crayon-purple: #cbbfe0;
@include stage($crayon-paper);
.listcrayon {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: $sp-2;
width: clamp(220px, 80vw, 300px);
}
.listcrayon__row {
display: flex;
align-items: center;
gap: $sp-3;
border-radius: $r-pill;
background: $stage-paper;
border: 2px solid rgba($crayon-ink, .16);
padding: $sp-2 $sp-4;
}
.listcrayon__check {
flex-shrink: 0;
width: 24px;
height: 20px;
overflow: visible;
}
.listcrayon__check-path {
fill: none;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 26;
animation: checkDraw $duration $easing infinite;
}
.listcrayon__row--1 .listcrayon__check-path { stroke: $crayon-red; animation-delay: 0ms; }
.listcrayon__row--2 .listcrayon__check-path { stroke: $crayon-blue; animation-delay: 160ms; }
.listcrayon__row--3 .listcrayon__check-path { stroke: $crayon-green; animation-delay: 320ms; }
.listcrayon__row--4 .listcrayon__check-path { stroke: $crayon-purple; animation-delay: 480ms; }
.listcrayon__text {
color: $crayon-ink;
font-weight: 600;
font-size: clamp(11px, 2.8vw, 13px);
}
// 데모: 체크 표시가 왼쪽부터 오른쪽까지 한 획으로 그려졌다 지워지길 반복하며, 행마다 시작이 어긋난다
@keyframes checkDraw {
0% { stroke-dashoffset: 26; opacity: 0; }
6% { opacity: 1; }
45% { stroke-dashoffset: 0; opacity: 1; }
80% { stroke-dashoffset: 0; opacity: 1; }
100% { stroke-dashoffset: -26; opacity: 0; }
}
@media (max-width: 340px), (max-height: 220px) {
.listcrayon { gap: $sp-1; }
.listcrayon__row { padding: $sp-1 $sp-3; gap: $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.listcrayon__check-path {
animation: none !important;
stroke-dashoffset: 0;
opacity: 1;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fff8ef;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.listcrayon {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
width: clamp(220px, 80vw, 300px);
}
.listcrayon__row {
display: flex;
align-items: center;
gap: 12px;
border-radius: 999px;
background: #ffffff;
border: 2px solid rgba(51, 38, 28, 0.16);
padding: 8px 16px;
}
.listcrayon__check {
flex-shrink: 0;
width: 24px;
height: 20px;
overflow: visible;
}
.listcrayon__check-path {
fill: none;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 26;
animation: checkDraw 2s ease-out infinite;
}
.listcrayon__row--1 .listcrayon__check-path {
stroke: #dbb6b7;
animation-delay: 0ms;
}
.listcrayon__row--2 .listcrayon__check-path {
stroke: #9cb8ce;
animation-delay: 160ms;
}
.listcrayon__row--3 .listcrayon__check-path {
stroke: #6dbe6d;
animation-delay: 320ms;
}
.listcrayon__row--4 .listcrayon__check-path {
stroke: #cbbfe0;
animation-delay: 480ms;
}
.listcrayon__text {
color: #33261c;
font-weight: 600;
font-size: clamp(11px, 2.8vw, 13px);
}
@keyframes checkDraw {
0% {
stroke-dashoffset: 26;
opacity: 0;
}
6% {
opacity: 1;
}
45% {
stroke-dashoffset: 0;
opacity: 1;
}
80% {
stroke-dashoffset: 0;
opacity: 1;
}
100% {
stroke-dashoffset: -26;
opacity: 0;
}
}
@media (max-width: 340px), (max-height: 220px) {
.listcrayon {
gap: 4px;
}
.listcrayon__row {
padding: 4px 12px;
gap: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.listcrayon__check-path {
animation: none !important;
stroke-dashoffset: 0;
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./FeatureListCrayon.module.scss";
export type FeatureListCrayonProps = {
items?: string[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const DEFAULT_ITEMS = ["드래그로 배치", "색상 골라 칠하기", "손그림 아이콘 고르기", "바로 공유하기"];
const COLORS = ["#ff5a5f", "#4fa3e3", "#6dbe6d", "#a685e2"];
export default function FeatureListCrayon({
items = DEFAULT_ITEMS,
duration = 2000,
easing = "ease-out",
color = "#ff5a5f",
}: FeatureListCrayonProps) {
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.row}>
<svg className={styles.check} viewBox="0 0 20 16" aria-hidden="true">
<path
className={styles.checkPath}
style={{ stroke: COLORS[i % COLORS.length], animationDelay: `${i * 160}ms` }}
d="M2 9 C 3 8, 4 10, 7 13 C 10 10, 14 6, 18 2"
/>
</svg>
<span className={styles.text}>{text}</span>
</li>
))}
</ul>
);
}04 · 手書きチェックリスト — 計測 累積面積 0.372% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードのどこを見るか
行ごとに色と開始時刻が違うので、チェックがそれぞれのリズムで完成します。
<svg class="check" viewBox="0 0 20 16">
<path fill="none" stroke="#ff5a5f" stroke-width="3"
stroke-linecap="round" stroke-linejoin="round"
stroke-dasharray="26"
d="M2 9 C 3 8, 4 10, 7 13 C 10 10, 14 6, 18 2" />
</svg>
<style>
.check path { animation: checkDraw 2s ease-out infinite; }
@keyframes checkDraw {
0% { stroke-dashoffset: 26; opacity: 0; }
45% { stroke-dashoffset: 0; opacity: 1; }
100% { stroke-dashoffset: -26; opacity: 0; }
}
</style>
05星と丸の料金プラン
画面で起きること
3つのプランカードはそれぞれ違うクレヨントーンで、おすすめプランの価格数字だけ手描きの丸が自分でぐるっと囲み、横で小さな星が2つ別々のリズムで瞬きます。

<main class="stage">
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<filter id="rough5" x="-40%" y="-40%" width="180%" height="180%">
<feTurbulence type="fractalNoise" baseFrequency="0.05 0.08" numOctaves="2" seed="21" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="5" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<div class="pricecrayon">
<div class="pricecrayon__card">
<p class="pricecrayon__name">スプラウト</p>
<p class="pricecrayon__price">無料</p>
<p class="pricecrayon__note">ずっと無料</p>
</div>
<div class="pricecrayon__card pricecrayon__card--main">
<span class="pricecrayon__star pricecrayon__star--a" aria-hidden="true">✦</span>
<span class="pricecrayon__star pricecrayon__star--b" aria-hidden="true">✦</span>
<p class="pricecrayon__name">クレヨン</p>
<span class="pricecrayon__price-wrap">
<svg class="pricecrayon__ring" viewBox="0 0 100 46" aria-hidden="true">
<path class="pricecrayon__ring-path" d="M50 4 C 22 3, 5 14, 6 23 C 7 34, 26 42, 50 42 C 74 42, 93 34, 94 23 C 95 13, 78 3, 50 5" />
</svg>
<p class="pricecrayon__price">月12,000ウォン</p>
</span>
<p class="pricecrayon__note">一番人気</p>
</div>
<div class="pricecrayon__card">
<p class="pricecrayon__name">ファミリー</p>
<p class="pricecrayon__price">月9,000ウォン</p>
<p class="pricecrayon__note">3人以上</p>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 동그라미가 한 번 그려지는 주기
$easing: $ease-out; // 선이 그려지는 곡선
$color: $stage-ink; // 글자 색 기본값(토큰) — 아래 크레용 잉크로 덮는다
$crayon-paper: #fff8ef;
$crayon-ink: #33261c;
$crayon-red: #dbb6b7;
$crayon-yellow: #d1c5a2;
$crayon-blue: #9cb8ce;
$crayon-green: #6dbe6d;
$crayon-purple: #cbbfe0;
$crayon-yellow-tint: #fff3cc;
$crayon-blue-tint: #dff0fc;
$crayon-green-tint: #e2f5e2;
@include stage($crayon-paper);
.pricecrayon {
display: flex;
align-items: center;
gap: $sp-3;
width: clamp(240px, 86vw, 360px);
}
.pricecrayon__card {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-1;
border-radius: $r-card;
padding: $sp-4 $sp-2;
text-align: center;
background: $crayon-blue-tint;
}
.pricecrayon__card:first-child { background: $crayon-green-tint; }
.pricecrayon__card:last-child { background: $crayon-blue-tint; }
.pricecrayon__card--main {
background: $crayon-yellow-tint;
border: 3px solid $crayon-ink;
transform: scale(1.1);
z-index: 1;
}
.pricecrayon__name {
margin: 0;
color: $crayon-ink;
font-weight: 700;
font-size: clamp(10px, 2.6vw, 12px);
}
.pricecrayon__price-wrap {
position: relative;
display: inline-flex;
}
.pricecrayon__price {
margin: 0;
position: relative;
z-index: 1;
color: $crayon-ink;
font-weight: 700;
font-size: clamp(12px, 3vw, 14px);
padding: $sp-1 $sp-1;
}
.pricecrayon__note {
margin: 0;
color: rgba($crayon-ink, .72);
font-weight: 500;
font-size: 10px;
}
.pricecrayon__ring {
position: absolute;
inset: -6px -10px;
width: calc(100% + 20px);
height: calc(100% + 12px);
pointer-events: none;
filter: url(#rough5);
}
.pricecrayon__ring-path {
fill: none;
stroke: $crayon-red;
stroke-width: 3.2;
stroke-linecap: round;
stroke-dasharray: 190;
animation: ringDraw $duration $easing infinite;
}
.pricecrayon__star {
position: absolute;
color: $crayon-yellow;
font-size: 14px;
line-height: 1;
animation: starTwinkle $duration $easing infinite;
}
.pricecrayon__star--a { top: -6px; left: 2px; animation-delay: 0ms; }
.pricecrayon__star--b { top: 4px; right: -4px; animation-delay: 520ms; }
// 데모: 추천 플랜 가격을 손그림 동그라미가 둘러 그렸다 지우기를 반복하고, 옆의 별이 번갈아 반짝인다
@keyframes ringDraw {
0% { stroke-dashoffset: 190; }
50% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -190; }
}
@keyframes starTwinkle {
0%, 100% { opacity: .25; transform: scale(.8); }
50% { opacity: 1; transform: scale(1.15); }
}
@media (max-width: 340px), (max-height: 220px) {
.pricecrayon { gap: $sp-1; }
.pricecrayon__card { padding: $sp-2 $sp-1; }
.pricecrayon__card--main { transform: scale(1.05); }
.pricecrayon__note { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.pricecrayon__ring-path, .pricecrayon__star {
animation: none !important;
}
.pricecrayon__ring-path { stroke-dashoffset: 0; }
.pricecrayon__star { 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: #fff8ef;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.pricecrayon {
display: flex;
align-items: center;
gap: 12px;
width: clamp(240px, 86vw, 360px);
}
.pricecrayon__card {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
border-radius: 16px;
padding: 16px 8px;
text-align: center;
background: #dff0fc;
}
.pricecrayon__card:first-child {
background: #e2f5e2;
}
.pricecrayon__card:last-child {
background: #dff0fc;
}
.pricecrayon__card--main {
background: #fff3cc;
border: 3px solid #33261c;
transform: scale(1.1);
z-index: 1;
}
.pricecrayon__name {
margin: 0;
color: #33261c;
font-weight: 700;
font-size: clamp(10px, 2.6vw, 12px);
}
.pricecrayon__price-wrap {
position: relative;
display: inline-flex;
}
.pricecrayon__price {
margin: 0;
position: relative;
z-index: 1;
color: #33261c;
font-weight: 700;
font-size: clamp(12px, 3vw, 14px);
padding: 4px 4px;
}
.pricecrayon__note {
margin: 0;
color: rgba(51, 38, 28, 0.72);
font-weight: 500;
font-size: 10px;
}
.pricecrayon__ring {
position: absolute;
inset: -6px -10px;
width: calc(100% + 20px);
height: calc(100% + 12px);
pointer-events: none;
filter: url(#rough5);
}
.pricecrayon__ring-path {
fill: none;
stroke: #dbb6b7;
stroke-width: 3.2;
stroke-linecap: round;
stroke-dasharray: 190;
animation: ringDraw 2s ease-out infinite;
}
.pricecrayon__star {
position: absolute;
color: #d1c5a2;
font-size: 14px;
line-height: 1;
animation: starTwinkle 2s ease-out infinite;
}
.pricecrayon__star--a {
top: -6px;
left: 2px;
animation-delay: 0ms;
}
.pricecrayon__star--b {
top: 4px;
right: -4px;
animation-delay: 520ms;
}
@keyframes ringDraw {
0% {
stroke-dashoffset: 190;
}
50% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -190;
}
}
@keyframes starTwinkle {
0%, 100% {
opacity: 0.25;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1.15);
}
}
@media (max-width: 340px), (max-height: 220px) {
.pricecrayon {
gap: 4px;
}
.pricecrayon__card {
padding: 8px 4px;
}
.pricecrayon__card--main {
transform: scale(1.05);
}
.pricecrayon__note {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.pricecrayon__ring-path, .pricecrayon__star {
animation: none !important;
}
.pricecrayon__ring-path {
stroke-dashoffset: 0;
}
.pricecrayon__star {
opacity: 1;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./PricingCrayon.module.scss";
export type PricingCrayonProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PricingCrayon({
duration = 2000,
easing = "ease-out",
color = "#ff5a5f",
}: PricingCrayonProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.wrap} style={vars}>
<svg width="0" height="0" style={{ position: "absolute" }} aria-hidden="true" focusable="false">
<filter id="pc-rough" x="-40%" y="-40%" width="180%" height="180%">
<feTurbulence type="fractalNoise" baseFrequency={0.06} numOctaves={2} seed={21} result="n" />
<feDisplacementMap in="SourceGraphic" in2="n" scale={5} xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>
<div className={`${styles.card} ${styles.green}`}>
<p className={styles.name}>새싹</p>
<p className={styles.price}>무료</p>
<p className={styles.note}>영원히</p>
</div>
<div className={`${styles.card} ${styles.main}`}>
<span className={`${styles.star} ${styles.starA}`} aria-hidden="true">✦</span>
<span className={`${styles.star} ${styles.starB}`} aria-hidden="true">✦</span>
<p className={styles.name}>크레용</p>
<span className={styles.priceWrap}>
<svg className={styles.ring} viewBox="0 0 100 46" aria-hidden="true">
<path className={styles.ringPath} d="M50 4 C 22 3, 5 14, 6 23 C 7 34, 26 42, 50 42 C 74 42, 93 34, 94 23 C 95 13, 78 3, 50 5" />
</svg>
<p className={styles.price}>월 12,000원</p>
</span>
<p className={styles.note}>가장 인기</p>
</div>
<div className={`${styles.card} ${styles.blue}`}>
<p className={styles.name}>패밀리</p>
<p className={styles.price}>월 9,000원</p>
<p className={styles.note}>3인 이상</p>
</div>
</div>
);
}05 · 星と丸の料金プラン — 計測 累積面積 0.613% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードの読み方
表がなくても色と丸だけでどれを選べばいいか伝わります。
.ring-path {
fill: none;
stroke: #ff5a5f;
stroke-width: 3.2;
stroke-dasharray: 190;
animation: ringDraw 2s ease-out infinite;
}
@keyframes ringDraw {
0% { stroke-dashoffset: 190; }
50% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -190; }
}
06クレヨン吹き出しのレビュー
どんな動きか
レビューはきれいな四角ではなく、輪郭がでこぼこしたSVGパスに収まり、しっぽもなめらかな三角形の代わりに折れた線2本で描きます。

<main class="stage">
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<filter id="rough6" x="-30%" y="-30%" width="160%" height="160%">
<feTurbulence type="fractalNoise" baseFrequency="0.045 0.06" numOctaves="2" seed="33" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="6" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<figure class="testicrayon">
<span class="testicrayon__avatar" aria-hidden="true"></span>
<div class="testicrayon__bubble">
<svg class="testicrayon__frame" viewBox="0 0 260 110" preserveAspectRatio="none" aria-hidden="true">
<path class="testicrayon__frame-path" d="M18 10 L242 10 C 250 10 252 18 252 26 L252 78 C 252 88 248 96 238 96 L48 96 L30 110 L34 94 L18 92 C 10 90 8 82 8 74 L8 26 C 8 16 10 10 18 10 Z" />
</svg>
<blockquote class="testicrayon__quote">うちのクラスのサイトみたい</blockquote>
<div class="testicrayon__stars" aria-hidden="true">
<span class="testicrayon__star" aria-hidden="true" style="--fill:100%">★</span>
<span class="testicrayon__star" aria-hidden="true" style="--fill:100%">★</span>
<span class="testicrayon__star" aria-hidden="true" style="--fill:100%">★</span>
<span class="testicrayon__star" aria-hidden="true" style="--fill:80%">★</span>
<span class="testicrayon__star" aria-hidden="true" style="--fill:40%">★</span>
</div>
<figcaption class="testicrayon__caption">キム・ドダム・美術学院院長</figcaption>
</div>
</figure>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 말풍선이 한 번 눌렸다 펴지는 주기
$easing: $ease-spring; // 눌림 곡선
$color: $stage-ink; // 글자 색 기본값(토큰) — 아래 크레용 잉크로 덮는다
$crayon-paper: #fff8ef;
$crayon-ink: #33261c;
$crayon-red: #dbb6b7;
$crayon-yellow: #d1c5a2;
$crayon-blue: #9cb8ce;
$crayon-green: #6dbe6d;
$crayon-purple: #cbbfe0;
@include stage($crayon-paper);
.testicrayon {
margin: 0;
display: flex;
align-items: flex-start;
gap: $sp-3;
width: clamp(230px, 84vw, 340px);
}
.testicrayon__avatar {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 50%;
background: repeating-conic-gradient($crayon-blue 0deg 60deg, $crayon-green 60deg 120deg);
border: 2px solid $crayon-paper;
box-shadow: 0 0 0 2px $crayon-ink;
}
.testicrayon__bubble {
position: relative;
flex: 1;
padding: $sp-4 $sp-4 $sp-3;
animation: bubbleSquish $duration $easing infinite;
}
.testicrayon__frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
filter: url(#rough6);
}
.testicrayon__frame-path {
fill: $stage-paper;
stroke: $crayon-ink;
stroke-width: 3;
stroke-linejoin: round;
}
.testicrayon__quote {
position: relative;
margin: 0;
color: $crayon-ink;
font-weight: 700;
font-size: clamp(12px, 3vw, 14px);
line-height: 1.4;
}
.testicrayon__stars {
position: relative;
display: flex;
gap: $sp-1;
margin-top: $sp-1;
}
.testicrayon__star {
position: relative;
display: inline-block;
color: rgba($crayon-ink, .22);
font-size: 13px;
line-height: 1;
}
.testicrayon__star::after {
content: "★";
position: absolute;
inset: 0;
width: var(--fill);
overflow: hidden;
white-space: nowrap;
color: $crayon-yellow;
}
.testicrayon__caption {
position: relative;
margin: $sp-1 0 0;
color: rgba($crayon-ink, .72);
font-weight: 500;
font-size: 10px;
}
// 데모: 말풍선이 젤리처럼 세로로 눌렸다 펴지길 반복한다
@keyframes bubbleSquish {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(.92); }
}
@media (max-width: 340px), (max-height: 220px) {
.testicrayon { gap: $sp-2; }
.testicrayon__avatar { width: 32px; height: 32px; }
.testicrayon__bubble { padding: $sp-3 $sp-3 $sp-2; }
.testicrayon__caption { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.testicrayon__bubble { animation: none !important; transform: none; }
}@charset "UTF-8";
html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fff8ef;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.testicrayon {
margin: 0;
display: flex;
align-items: flex-start;
gap: 12px;
width: clamp(230px, 84vw, 340px);
}
.testicrayon__avatar {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 50%;
background: repeating-conic-gradient(#9cb8ce 0deg 60deg, #6dbe6d 60deg 120deg);
border: 2px solid #fff8ef;
box-shadow: 0 0 0 2px #33261c;
}
.testicrayon__bubble {
position: relative;
flex: 1;
padding: 16px 16px 12px;
animation: bubbleSquish 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.testicrayon__frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
filter: url(#rough6);
}
.testicrayon__frame-path {
fill: #ffffff;
stroke: #33261c;
stroke-width: 3;
stroke-linejoin: round;
}
.testicrayon__quote {
position: relative;
margin: 0;
color: #33261c;
font-weight: 700;
font-size: clamp(12px, 3vw, 14px);
line-height: 1.4;
}
.testicrayon__stars {
position: relative;
display: flex;
gap: 4px;
margin-top: 4px;
}
.testicrayon__star {
position: relative;
display: inline-block;
color: rgba(51, 38, 28, 0.22);
font-size: 13px;
line-height: 1;
}
.testicrayon__star::after {
content: "★";
position: absolute;
inset: 0;
width: var(--fill);
overflow: hidden;
white-space: nowrap;
color: #d1c5a2;
}
.testicrayon__caption {
position: relative;
margin: 4px 0 0;
color: rgba(51, 38, 28, 0.72);
font-weight: 500;
font-size: 10px;
}
@keyframes bubbleSquish {
0%, 100% {
transform: scaleY(1);
}
50% {
transform: scaleY(0.92);
}
}
@media (max-width: 340px), (max-height: 220px) {
.testicrayon {
gap: 8px;
}
.testicrayon__avatar {
width: 32px;
height: 32px;
}
.testicrayon__bubble {
padding: 12px 12px 8px;
}
.testicrayon__caption {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.testicrayon__bubble {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./TestimonialCrayon.module.scss";
export type TestimonialCrayonProps = {
quote?: string;
name?: string;
stars?: number[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TestimonialCrayon({
quote = "우리 반 홈페이지 같아요",
name = "김도담 · 미술학원 원장",
stars = [100, 100, 100, 80, 40],
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ff5a5f",
}: TestimonialCrayonProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<figure className={styles.figure} style={vars}>
<svg width="0" height="0" style={{ position: "absolute" }} aria-hidden="true" focusable="false">
<filter id="tc-rough" x="-30%" y="-30%" width="160%" height="160%">
<feTurbulence type="fractalNoise" baseFrequency={0.05} numOctaves={2} seed={33} result="n" />
<feDisplacementMap in="SourceGraphic" in2="n" scale={6} xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>
<span className={styles.avatar} aria-hidden="true" />
<div className={styles.bubble}>
<svg className={styles.frame} viewBox="0 0 260 110" preserveAspectRatio="none" aria-hidden="true">
<path
className={styles.framePath}
d="M18 10 L242 10 C 250 10 252 18 252 26 L252 78 C 252 88 248 96 238 96 L48 96 L30 110 L34 94 L18 92 C 10 90 8 82 8 74 L8 26 C 8 16 10 10 18 10 Z"
/>
</svg>
<blockquote className={styles.quote}>{quote}</blockquote>
<div className={styles.stars} aria-hidden="true">
{stars.map((fill, i) => (
<span key={i} className={styles.star} aria-hidden="true" style={{ "--fill": `${fill}%` } as CSSProperties}>★</span>
))}
</div>
<figcaption className={styles.caption}>{name}</figcaption>
</div>
</figure>
);
}06 · クレヨン吹き出しのレビュー — 計測 累積面積 3.608% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
どこを変えるか
星評価は5つの幅を100・100・100・80・40%と変え、本物のレビュー点数らしく見せました。
<svg class="frame" viewBox="0 0 260 110" preserveAspectRatio="none">
<path fill="#fff8ef" stroke="#33261c" stroke-width="3" stroke-linejoin="round"
d="M18 10 L242 10 C250 10 252 18 252 26 L252 78 C252 88 248 96 238 96
L48 96 L30 110 L34 94 L18 92 C10 90 8 82 8 74
L8 26 C8 16 10 10 18 10 Z" />
</svg>
07クレヨンハッチングCTA帯
何が見えるか
帯の背景は画像を使わずrepeating-linear-gradient1行だけで引いた細かい斜線ハッチングで、その上のボタンは赤い点線の枠がずっと行進するように描き直され続けます。

<main class="stage">
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<filter id="rough7" x="-30%" y="-30%" width="160%" height="160%">
<feTurbulence type="fractalNoise" baseFrequency="0.05 0.07" numOctaves="2" seed="44" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="5" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<section class="ctacrayon">
<h2 class="ctacrayon__title">今すぐクレヨンみたいに塗ってみよう</h2>
<p class="ctacrayon__desc">無料で試してみてください</p>
<span class="ctacrayon__btn-wrap">
<svg class="ctacrayon__btn-frame" viewBox="0 0 120 44" preserveAspectRatio="none" aria-hidden="true">
<rect x="4" y="4" width="112" height="36" rx="18" class="ctacrayon__btn-rect" />
</svg>
<button class="ctacrayon__btn" type="button">はじめる</button>
</span>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 버튼 테두리가 한 번 다시 그려지는 주기
$easing: $ease-spring; // 눌림 곡선
$color: $stage-ink; // 글자 색 기본값(토큰) — 아래 크레용 잉크로 덮는다
$crayon-paper: #fff8ef;
$crayon-ink: #33261c;
$crayon-red: #dbb6b7;
$crayon-yellow: #d1c5a2;
$crayon-blue: #9cb8ce;
$crayon-green: #6dbe6d;
$crayon-purple: #cbbfe0;
$crayon-purple-tint: #efe6fb;
@include stage($crayon-paper);
.ctacrayon {
position: relative;
width: clamp(230px, 86vw, 340px);
border-radius: $r-card;
padding: $sp-8 $sp-5;
text-align: center;
overflow: hidden;
background:
repeating-linear-gradient(45deg, rgba($crayon-ink, .08) 0 2px, transparent 2px 9px),
$crayon-purple-tint;
border: 3px solid $crayon-ink;
}
.ctacrayon__title {
margin: 0 0 $sp-1;
color: $crayon-ink;
font-weight: 700;
font-size: clamp(15px, 3.6vw, 19px);
line-height: 1.3;
}
.ctacrayon__desc {
margin: 0 0 $sp-4;
color: rgba($crayon-ink, .72);
font-weight: 500;
font-size: clamp(11px, 2.6vw, 13px);
}
.ctacrayon__btn-wrap {
position: relative;
display: inline-block;
}
.ctacrayon__btn-frame {
position: absolute;
inset: -6px;
width: calc(100% + 12px);
height: calc(100% + 12px);
pointer-events: none;
filter: url(#rough7);
}
.ctacrayon__btn-rect {
fill: none;
stroke: $crayon-red;
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 14 8;
animation: btnFrameMarch $duration linear infinite;
}
.ctacrayon__btn {
position: relative;
border: 0;
border-radius: $r-pill;
background: $stage-paper;
color: $crayon-ink;
padding: $sp-3 $sp-6;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 14px);
animation: btnPress $duration $easing infinite;
}
.ctacrayon__btn:focus-visible {
outline: 3px solid $crayon-ink;
outline-offset: 2px;
}
// 데모: 버튼 테두리가 크레용으로 계속 덧그려지고, 버튼 자체는 눌렸다 돌아오길 반복한다
@keyframes btnFrameMarch {
to { stroke-dashoffset: -440; }
}
@keyframes btnPress {
0%, 100% { transform: scale(1); }
50% { transform: scale(.94); }
}
@media (max-width: 340px), (max-height: 220px) {
.ctacrayon { padding: $sp-4 $sp-3; }
.ctacrayon__title { font-size: 13px; }
.ctacrayon__desc { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.ctacrayon__btn-rect, .ctacrayon__btn {
animation: none !important;
transform: none;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fff8ef;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ctacrayon {
position: relative;
width: clamp(230px, 86vw, 340px);
border-radius: 16px;
padding: 32px 20px;
text-align: center;
overflow: hidden;
background: repeating-linear-gradient(45deg, rgba(51, 38, 28, 0.08) 0 2px, transparent 2px 9px), #efe6fb;
border: 3px solid #33261c;
}
.ctacrayon__title {
margin: 0 0 4px;
color: #33261c;
font-weight: 700;
font-size: clamp(15px, 3.6vw, 19px);
line-height: 1.3;
}
.ctacrayon__desc {
margin: 0 0 16px;
color: rgba(51, 38, 28, 0.72);
font-weight: 500;
font-size: clamp(11px, 2.6vw, 13px);
}
.ctacrayon__btn-wrap {
position: relative;
display: inline-block;
}
.ctacrayon__btn-frame {
position: absolute;
inset: -6px;
width: calc(100% + 12px);
height: calc(100% + 12px);
pointer-events: none;
filter: url(#rough7);
}
.ctacrayon__btn-rect {
fill: none;
stroke: #dbb6b7;
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 14 8;
animation: btnFrameMarch 2s linear infinite;
}
.ctacrayon__btn {
position: relative;
border: 0;
border-radius: 999px;
background: #ffffff;
color: #33261c;
padding: 12px 24px;
font-weight: 700;
font-size: clamp(12px, 2.8vw, 14px);
animation: btnPress 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.ctacrayon__btn:focus-visible {
outline: 3px solid #33261c;
outline-offset: 2px;
}
@keyframes btnFrameMarch {
to {
stroke-dashoffset: -440;
}
}
@keyframes btnPress {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(0.94);
}
}
@media (max-width: 340px), (max-height: 220px) {
.ctacrayon {
padding: 16px 12px;
}
.ctacrayon__title {
font-size: 13px;
}
.ctacrayon__desc {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.ctacrayon__btn-rect, .ctacrayon__btn {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./CtaBandCrayon.module.scss";
export type CtaBandCrayonProps = {
title?: string;
desc?: string;
cta?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function CtaBandCrayon({
title = "지금 크레용처럼 칠해보세요",
desc = "무료로 시작해보세요",
cta = "시작하기",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ff5a5f",
}: CtaBandCrayonProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.band} style={vars}>
<svg width="0" height="0" style={{ position: "absolute" }} aria-hidden="true" focusable="false">
<filter id="cb-rough" x="-30%" y="-30%" width="160%" height="160%">
<feTurbulence type="fractalNoise" baseFrequency={0.06} numOctaves={2} seed={44} result="n" />
<feDisplacementMap in="SourceGraphic" in2="n" scale={5} xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>
<h2 className={styles.title}>{title}</h2>
<p className={styles.desc}>{desc}</p>
<span className={styles.btnWrap}>
<svg className={styles.btnFrame} viewBox="0 0 120 44" preserveAspectRatio="none" aria-hidden="true">
<rect x={4} y={4} width={112} height={36} rx={18} className={styles.btnRect} />
</svg>
<button type="button" className={styles.btn}>{cta}</button>
</span>
</section>
);
}07 · クレヨンハッチングCTA帯 — 計測 累積面積 1.203% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードのどこを見るか
塗り絵の1ページに置かれたボタンのように見えます。
.band {
background:
repeating-linear-gradient(45deg, rgba(51,38,28,.08) 0 2px, transparent 2px 9px),
#efe6fb;
}
.btn-rect {
fill: none; stroke: #ff5a5f; stroke-width: 3;
stroke-dasharray: 14 8;
animation: march 2s linear infinite;
}
@keyframes march { to { stroke-dashoffset: -440; } }
08はさみ切りジグザグフッター
画面で起きること
フッターの上辺は切り込みごとに要素を置かず、clip-path: polygon()1つで高さがばらばらな歯を描き、子どもがはさみで雑に切ったように見せます。

<main class="stage">
<div class="footcrayon">
<div class="footcrayon__zigzag" aria-hidden="true"></div>
<footer class="footcrayon__panel">
<div class="footcrayon__col footcrayon__col--a">
<p class="footcrayon__head">クラス</p>
<p class="footcrayon__link">授業</p>
<p class="footcrayon__link">資料</p>
</div>
<div class="footcrayon__col footcrayon__col--b">
<p class="footcrayon__head">ブランド</p>
<p class="footcrayon__link">紹介</p>
<p class="footcrayon__link">お問い合わせ</p>
</div>
<div class="footcrayon__col footcrayon__col--c">
<p class="footcrayon__head">ヘルプ</p>
<p class="footcrayon__link">よくある質問</p>
<p class="footcrayon__link">ガイド</p>
</div>
<p class="footcrayon__bottom">
クレヨン・今日も一枚の絵
<span class="footcrayon__star footcrayon__star--a" aria-hidden="true">✦</span>
<span class="footcrayon__star footcrayon__star--b" aria-hidden="true">✦</span>
</p>
</footer>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 별이 한 번 반짝이는 주기
$easing: $ease-spring; // 반짝임 곡선
$color: $stage-ink; // 글자 색 기본값(토큰) — 아래 크레용 잉크로 덮는다
$crayon-paper: #fff8ef;
$crayon-ink: #33261c;
$crayon-red: #dbb6b7;
$crayon-yellow: #d1c5a2;
$crayon-blue: #9cb8ce;
$crayon-green: #6dbe6d;
$crayon-purple: #cbbfe0;
$crayon-red-tint: #ffe1e1;
$crayon-yellow-tint: #fff3cc;
$crayon-blue-tint: #dff0fc;
@include stage($crayon-paper);
.footcrayon {
width: clamp(240px, 90vw, 360px);
}
// 가위로 대충 오린 듯한 지그재그 윗변 — 조각마다 요소를 두지 않고 clip-path 하나로 그린다
.footcrayon__zigzag {
height: 14px;
background: $crayon-ink;
clip-path: polygon(
0% 100%, 0% 30%, 6% 85%, 12% 15%, 18% 90%, 24% 20%, 30% 88%,
36% 12%, 42% 92%, 48% 18%, 54% 85%, 60% 22%, 66% 90%, 72% 15%,
78% 88%, 84% 20%, 90% 92%, 96% 14%, 100% 30%, 100% 100%
);
}
.footcrayon__panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $sp-2;
padding: $sp-4 $sp-3;
background: $crayon-ink;
}
.footcrayon__col {
display: flex;
flex-direction: column;
gap: $sp-1;
border-radius: $r-sm;
padding: $sp-2;
}
.footcrayon__col--a { background: rgba($crayon-red-tint, .18); }
.footcrayon__col--b { background: rgba($crayon-yellow-tint, .18); }
.footcrayon__col--c { background: rgba($crayon-blue-tint, .18); }
.footcrayon__head {
margin: 0 0 $sp-1;
color: $crayon-paper;
font-weight: 700;
font-size: 10px;
}
.footcrayon__link {
margin: 0;
color: rgba($crayon-paper, .72);
font-weight: 500;
font-size: 9px;
}
.footcrayon__bottom {
position: relative;
grid-column: 1 / -1;
margin: $sp-2 0 0;
text-align: center;
color: rgba($crayon-paper, .72);
font-weight: 500;
font-size: 10px;
}
.footcrayon__star {
display: inline-block;
color: $crayon-yellow;
margin-left: 4px;
font-size: 20px;
animation: starBlink $duration $easing infinite;
}
.footcrayon__star--a { animation-delay: 0ms; }
.footcrayon__star--b { animation-delay: 600ms; margin-left: $sp-1; }
// 데모: 작은 별 두 개가 서로 다른 박자로 반짝인다
@keyframes starBlink {
0%, 100% { opacity: .15; transform: scale(.5); }
50% { opacity: 1; transform: scale(1.7); }
}
@media (max-width: 340px), (max-height: 220px) {
.footcrayon__panel { grid-template-columns: repeat(3, 1fr); gap: $sp-1; padding: $sp-2; }
.footcrayon__link { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.footcrayon__star { 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: #fff8ef;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.footcrayon {
width: clamp(240px, 90vw, 360px);
}
.footcrayon__zigzag {
height: 14px;
background: #33261c;
clip-path: polygon(0% 100%, 0% 30%, 6% 85%, 12% 15%, 18% 90%, 24% 20%, 30% 88%, 36% 12%, 42% 92%, 48% 18%, 54% 85%, 60% 22%, 66% 90%, 72% 15%, 78% 88%, 84% 20%, 90% 92%, 96% 14%, 100% 30%, 100% 100%);
}
.footcrayon__panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding: 16px 12px;
background: #33261c;
}
.footcrayon__col {
display: flex;
flex-direction: column;
gap: 4px;
border-radius: 8px;
padding: 8px;
}
.footcrayon__col--a {
background: rgba(255, 225, 225, 0.18);
}
.footcrayon__col--b {
background: rgba(255, 243, 204, 0.18);
}
.footcrayon__col--c {
background: rgba(223, 240, 252, 0.18);
}
.footcrayon__head {
margin: 0 0 4px;
color: #fff8ef;
font-weight: 700;
font-size: 10px;
}
.footcrayon__link {
margin: 0;
color: rgba(255, 248, 239, 0.72);
font-weight: 500;
font-size: 9px;
}
.footcrayon__bottom {
position: relative;
grid-column: 1/-1;
margin: 8px 0 0;
text-align: center;
color: rgba(255, 248, 239, 0.6);
font-weight: 500;
font-size: 10px;
}
.footcrayon__star {
display: inline-block;
color: #d1c5a2;
margin-left: 4px;
font-size: 20px;
animation: starBlink 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.footcrayon__star--a {
animation-delay: 0ms;
}
.footcrayon__star--b {
animation-delay: 600ms;
margin-left: 4px;
}
@keyframes starBlink {
0%, 100% {
opacity: 0.15;
transform: scale(0.5);
}
50% {
opacity: 1;
transform: scale(1.7);
}
}
@media (max-width: 340px), (max-height: 220px) {
.footcrayon__panel {
grid-template-columns: repeat(3, 1fr);
gap: 4px;
padding: 8px;
}
.footcrayon__link {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.footcrayon__star {
animation: none !important;
opacity: 1;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./FooterCrayon.module.scss";
export type FooterCrayonProps = {
bottom?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function FooterCrayon({
bottom = "크레용 · 오늘도 그림 한 장",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffcd3c",
}: FooterCrayonProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.wrap} style={vars}>
<div className={styles.zigzag} aria-hidden="true" />
<footer className={styles.panel}>
<div className={`${styles.col} ${styles.colA}`}>
<p className={styles.head}>클래스</p>
<p className={styles.link}>수업</p>
<p className={styles.link}>자료</p>
</div>
<div className={`${styles.col} ${styles.colB}`}>
<p className={styles.head}>브랜드</p>
<p className={styles.link}>소개</p>
<p className={styles.link}>문의</p>
</div>
<div className={`${styles.col} ${styles.colC}`}>
<p className={styles.head}>도움</p>
<p className={styles.link}>자주 묻는 질문</p>
<p className={styles.link}>가이드</p>
</div>
<p className={styles.bottom}>
{bottom}
<span className={`${styles.star} ${styles.starA}`} aria-hidden="true">✦</span>
<span className={`${styles.star} ${styles.starB}`} aria-hidden="true">✦</span>
</p>
</footer>
</div>
);
}08 · はさみ切りジグザグフッター — 計測 累積面積 0.346% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
コードの読み方
均一なジグザグではなく点ごとに高さが違うことがポイントです。
.zigzag {
height: 14px;
background: #33261c;
clip-path: polygon(
0% 100%, 0% 30%, 6% 85%, 12% 15%, 18% 90%, 24% 20%,
30% 88%, 36% 12%, 42% 92%, 48% 18%, 54% 85%, 60% 22%,
66% 90%, 72% 15%, 78% 88%, 84% 20%, 90% 92%, 96% 14%,
100% 30%, 100% 100%
);
}
09二重線クレヨンボタン・入力欄
どんな動きか
ボタンは赤と青の二重線がループで少しずれては戻り、入力欄は手で描いたような青い丸が外側へにじむように広がって消えることをくり返します。

<main class="stage">
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<filter id="rough9" x="-40%" y="-40%" width="180%" height="180%">
<feTurbulence type="fractalNoise" baseFrequency="0.05 0.07" numOctaves="2" seed="51" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="5" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<form class="formcrayon">
<label class="formcrayon__field">
<span class="formcrayon__label">お名前</span>
<span class="formcrayon__input-wrap">
<svg class="formcrayon__ring" viewBox="0 0 100 40" preserveAspectRatio="none" aria-hidden="true">
<rect x="3" y="3" width="94" height="34" rx="10" class="formcrayon__ring-rect" />
</svg>
<input class="formcrayon__input" type="text" placeholder="お名前を入力" readonly>
</span>
</label>
<span class="formcrayon__btn-wrap">
<svg class="formcrayon__btn-frame" viewBox="0 0 110 40" preserveAspectRatio="none" aria-hidden="true">
<rect x="5" y="5" width="100" height="30" rx="15" class="formcrayon__btn-rect formcrayon__btn-rect--a" />
<rect x="3" y="3" width="100" height="30" rx="15" class="formcrayon__btn-rect formcrayon__btn-rect--b" />
</svg>
<button class="formcrayon__btn" type="button">保存する</button>
</span>
</form>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 이중선이 한 번 어긋났다 돌아오는 주기
$easing: $ease-spring; // 어긋남 곡선
$color: $stage-ink; // 글자 색 기본값(토큰) — 아래 크레용 잉크로 덮는다
$crayon-paper: #fff8ef;
$crayon-ink: #33261c;
$crayon-red: #dbb6b7;
$crayon-yellow: #d1c5a2;
$crayon-blue: #9cb8ce;
$crayon-green: #6dbe6d;
$crayon-purple: #cbbfe0;
@include stage($crayon-paper);
.formcrayon {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $sp-4;
width: clamp(210px, 74vw, 260px);
}
.formcrayon__field {
display: flex;
flex-direction: column;
gap: $sp-1;
width: 100%;
}
.formcrayon__label {
color: $crayon-ink;
font-weight: 700;
font-size: 11px;
}
.formcrayon__input-wrap {
position: relative;
display: block;
}
.formcrayon__ring {
position: absolute;
inset: -8px;
width: calc(100% + 16px);
height: calc(100% + 16px);
pointer-events: none;
filter: url(#rough9);
}
.formcrayon__ring-rect {
fill: none;
stroke: $crayon-blue;
stroke-width: 3;
stroke-linecap: round;
animation: ringBloom $duration $easing infinite;
}
.formcrayon__input {
position: relative;
box-sizing: border-box;
width: 100%;
border: 2px solid $crayon-ink;
border-radius: $r-control;
background: $stage-paper;
color: $crayon-ink;
padding: $sp-2 $sp-3;
font-weight: 500;
font-size: 12px;
}
.formcrayon__input:focus-visible {
outline: 3px solid $crayon-blue;
outline-offset: 2px;
}
.formcrayon__btn-wrap {
position: relative;
display: inline-block;
}
.formcrayon__btn-frame {
position: absolute;
inset: -6px;
width: calc(100% + 12px);
height: calc(100% + 12px);
pointer-events: none;
filter: url(#rough9);
}
.formcrayon__btn-rect {
fill: none;
stroke-width: 2.6;
}
.formcrayon__btn-rect--a { stroke: $crayon-red; animation: lineShiftA $duration $easing infinite; }
.formcrayon__btn-rect--b { stroke: $crayon-blue; opacity: .85; animation: lineShiftB $duration $easing infinite; }
.formcrayon__btn {
position: relative;
border: 0;
border-radius: $r-pill;
background: $crayon-yellow;
color: $crayon-ink;
padding: $sp-3 $sp-6;
font-weight: 700;
font-size: 13px;
}
.formcrayon__btn:focus-visible {
outline: 3px solid $crayon-ink;
outline-offset: 2px;
}
// 데모: 인풋은 손으로 두른 동그라미가 번지듯 피어났다 사라지고, 버튼은 두 선이 살짝 어긋났다 돌아온다
@keyframes ringBloom {
0% { opacity: 0; transform: scale(.9); }
35% { opacity: 1; transform: scale(1); }
70% { opacity: 1; transform: scale(1.03); }
100% { opacity: 0; transform: scale(1.06); }
}
@keyframes lineShiftA {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-2px, 1px); }
}
@keyframes lineShiftB {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(2px, -1px); }
}
@media (max-width: 340px), (max-height: 220px) {
.formcrayon { gap: $sp-2; }
.formcrayon__btn { padding: $sp-2 $sp-4; }
}
@media (prefers-reduced-motion: reduce) {
.formcrayon__ring-rect, .formcrayon__btn-rect {
animation: none !important;
transform: none;
}
.formcrayon__ring-rect { opacity: .8; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fff8ef;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.formcrayon {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
width: clamp(210px, 74vw, 260px);
}
.formcrayon__field {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
}
.formcrayon__label {
color: #33261c;
font-weight: 700;
font-size: 11px;
}
.formcrayon__input-wrap {
position: relative;
display: block;
}
.formcrayon__ring {
position: absolute;
inset: -8px;
width: calc(100% + 16px);
height: calc(100% + 16px);
pointer-events: none;
filter: url(#rough9);
}
.formcrayon__ring-rect {
fill: none;
stroke: #9cb8ce;
stroke-width: 3;
stroke-linecap: round;
animation: ringBloom 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.formcrayon__input {
position: relative;
box-sizing: border-box;
width: 100%;
border: 2px solid #33261c;
border-radius: 12px;
background: #ffffff;
color: #33261c;
padding: 8px 12px;
font-weight: 500;
font-size: 12px;
}
.formcrayon__input:focus-visible {
outline: 3px solid #9cb8ce;
outline-offset: 2px;
}
.formcrayon__btn-wrap {
position: relative;
display: inline-block;
}
.formcrayon__btn-frame {
position: absolute;
inset: -6px;
width: calc(100% + 12px);
height: calc(100% + 12px);
pointer-events: none;
filter: url(#rough9);
}
.formcrayon__btn-rect {
fill: none;
stroke-width: 2.6;
}
.formcrayon__btn-rect--a {
stroke: #dbb6b7;
animation: lineShiftA 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.formcrayon__btn-rect--b {
stroke: #9cb8ce;
opacity: 0.85;
animation: lineShiftB 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.formcrayon__btn {
position: relative;
border: 0;
border-radius: 999px;
background: #d1c5a2;
color: #33261c;
padding: 12px 24px;
font-weight: 700;
font-size: 13px;
}
.formcrayon__btn:focus-visible {
outline: 3px solid #33261c;
outline-offset: 2px;
}
@keyframes ringBloom {
0% {
opacity: 0;
transform: scale(0.9);
}
35% {
opacity: 1;
transform: scale(1);
}
70% {
opacity: 1;
transform: scale(1.03);
}
100% {
opacity: 0;
transform: scale(1.06);
}
}
@keyframes lineShiftA {
0%, 100% {
transform: translate(0, 0);
}
50% {
transform: translate(-2px, 1px);
}
}
@keyframes lineShiftB {
0%, 100% {
transform: translate(0, 0);
}
50% {
transform: translate(2px, -1px);
}
}
@media (max-width: 340px), (max-height: 220px) {
.formcrayon {
gap: 8px;
}
.formcrayon__btn {
padding: 8px 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.formcrayon__ring-rect, .formcrayon__btn-rect {
animation: none !important;
transform: none;
}
.formcrayon__ring-rect {
opacity: 0.8;
}
}import type { CSSProperties } from "react";
import styles from "./ButtonsInputsCrayon.module.scss";
export type ButtonsInputsCrayonProps = {
label?: string;
placeholder?: string;
cta?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ButtonsInputsCrayon({
label = "이름",
placeholder = "이름을 적어보세요",
cta = "저장하기",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#4fa3e3",
}: ButtonsInputsCrayonProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<form className={styles.form} style={vars}>
<svg width="0" height="0" style={{ position: "absolute" }} aria-hidden="true" focusable="false">
<filter id="bi-rough" x="-40%" y="-40%" width="180%" height="180%">
<feTurbulence type="fractalNoise" baseFrequency={0.06} numOctaves={2} seed={51} result="n" />
<feDisplacementMap in="SourceGraphic" in2="n" scale={5} xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>
<label className={styles.field}>
<span className={styles.label}>{label}</span>
<span className={styles.inputWrap}>
<svg className={styles.ring} viewBox="0 0 100 40" preserveAspectRatio="none" aria-hidden="true">
<rect x={3} y={3} width={94} height={34} rx={10} className={styles.ringRect} />
</svg>
<input className={styles.input} type="text" placeholder={placeholder} />
</span>
</label>
<span className={styles.btnWrap}>
<svg className={styles.btnFrame} viewBox="0 0 110 40" preserveAspectRatio="none" aria-hidden="true">
<rect x={5} y={5} width={100} height={30} rx={15} className={`${styles.btnRect} ${styles.btnRectA}`} />
<rect x={3} y={3} width={100} height={30} rx={15} className={`${styles.btnRect} ${styles.btnRectB}`} />
</svg>
<button type="button" className={styles.btn}>{cta}</button>
</span>
</form>
);
}09 · 二重線クレヨンボタン・入力欄 — 計測 累積面積 3.212% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
どこを変えるか
.btn-rect-a { stroke: #ff5a5f; animation: shiftA 2s infinite; }
.btn-rect-b { stroke: #4fa3e3; opacity: .85; animation: shiftB 2s infinite; }
@keyframes shiftA { 50% { transform: translate(-2px, 1px); } }
@keyframes shiftB { 50% { transform: translate(2px, -1px); } }
.ring-rect { animation: bloom 2s infinite; }
@keyframes bloom {
0%, 100% { opacity: 0; transform: scale(.9); }
50% { opacity: 1; transform: scale(1); }
}
tailwind.config カスタム
クレヨン7色と半径3種をここ1か所に書いておけば、1ページ版と部品9種が全部同じルールに従います。揺れフィルターのidは部品ごとに別の番号を振って衝突を避け、1ページ版だけ1つのidを共有します。
theme: {
extend: {
colors: {
paper: "#fff8ef", ink: "#33261c", red: "#ff5a5f",
yellow: "#ffcd3c", blue: "#4fa3e3", green: "#6dbe6d", purple: "#a685e2",
},
borderRadius: { crayon: "20px", control: "12px", pill: "999px" },
spacing: { gutter: "16px" },
},
},
バイブコーディング プロンプト
configをプロジェクトに入れたあとClaude Code・Cursorに渡す指示は、揺れフィルターをどこにかけてどこにかけないかが要です。指示文の全文はzipのprompt.mdに入っています。
背景は paper(#fff8ef) 1色に統一する。枠のある箱すべてに、手描き風の揺れる輪郭線
(SVG feTurbulence + feDisplacementMap) を別レイヤーとしてかける — 文字を包む
要素そのものには絶対にかけない。角丸は rounded-crayon か rounded-pill に統一。
二重線を使うときは赤と青を1〜2pxずらして重ねる。
この指示文をpage/index.htmlに実際に適用し、1280px幅の画面全体を撮影したのが下の画像です。

どこで壊れるか — 落とし穴
最初に実際にぶつかった落とし穴は、揺れフィルターをどこにかけるかでした。最初はカード全体(枠線と見出し文字ごと)をfilter: url(#rough)でまとめて包んだところ、feDisplacementMapがすべてのピクセルを同じノイズマップでずらすため見出し文字まで波打ち、小さいサイズでは読みにくくなりました。枠線を描くSVGのrect・pathだけを別レイヤーに切り離し、そのレイヤーだけにフィルターをかけ直すと、文字はくっきり残り枠線だけが揺れました。
2つ目は実測で見つかった数字です。フッターの星2つの瞬きだけで動きを測ったところ、変化した面積が0.07%しかなく、このサイトの下限(0.08%)に届かず静止画と区別がつきませんでした。星のサイズを14pxから20pxへ、拡大縮小の範囲を0.7〜1.2倍から0.5〜1.7倍に広げると0.35%まで上がり、通過しました。3つ目はアクティブなナビタブを囲む丸の大きさで、viewBoxを92×50で描いたときは隣のタブの文字まで丸がはみ出し、56×30に縮めてようやく「ホーム」の文字だけを正確に囲みました。クレヨンの線がメールアプリの中でどこまで生き残るかはメルマガ デザイン 9種で測り直しました。この3つを直した完成版は、このパスワード r6qyfm69 がかかったzipを開けば確認できます。
9つのデモすべてがMDNのprefers-reduced-motionメディアクエリを検知すると、線が描かれるアニメーションとカードが傾く動きを止め、各要素を完成した状態(チェックは完成形、丸は閉じた円)のまま固定します。
アクセシビリティ
手書き風デザインで最もよくある失敗は、揺れフィルターを文字そのものにかけてしまうことです。feDisplacementMapはソースの全ピクセルをノイズマップの値だけ押しのけるので、文字のストロークも例外ではなく、小さいサイズでは字形が崩れて読みにくくなります。このキットはフィルターを常に枠線専用のSVGレイヤーにだけかけ、文字要素には一切触れません。文字色は常に濃いインク色(#33261c)に固定し、紙の背景で13.9:1、キット内で最も暗いカードの淡色(パープルの淡色)の上でも12.1:1と、WCAG 2.1の最低コントラスト基準である4.5:1を大きく上回ります。赤・青の強調線は装飾的な枠線なので文字コントラストの対象ではありませんが、ボタン・入力欄のキーボードフォーカスは色線に頼らず、インク色の:focus-visibleアウトラインを別に描いています。
クレヨンではなく別の明るいトーンのホームページ流行も気になる方はパステルキャンディランディングもあわせてご覧ください。デザイントレンドカテゴリ全体とこのブログの紹介は紹介ページにまとめてあります。
FAQ
手書き風デザインと手書きフォントを使うのはどう違いますか?
手書きフォントは文字そのものの形を手書き風に変えますが、このキットは文字は整ったフォントのまま残し、枠線・下線・チェックといった装飾の線だけをSVGフィルターで揺らします。文字を揺らすと読みにくくなりますが、枠線だけ揺らせば可愛さは残り読みやすさも保たれます。同じ信頼の席をすっきりした方向で答えるキットがsaas サイト デザイン 信頼感キットです。
クレヨン デザイン イラスト風ですが、暗いトーンのブランドでも使えますか?
使えますが、紙色(#fff8ef)の代わりに暗い背景にする場合はインクの文字色も明るい色へ反転させないとコントラストが崩れます。赤・黄のような彩度の高いクレヨン線は暗い背景でもそのまま使えますが、カードの淡色は暗い背景用に組み直す必要があります。
feTurbulenceフィルターは重くなりませんか?
このキットのフィルターは枠線を描く小さなSVGレイヤー1つだけにかかり、値も固定されているため毎フレーム再計算されません — 見えている動きはstroke-dashoffsetとtransformが担当しています。カード全体やページ全体にフィルターをかけると重くなるので、常にこのキットのように小さな枠線レイヤーだけに絞って使います。









