Crayon Web Design: 9 Hand-Drawn Landing Sections
Crayon web design draws thick, wobbly outlines around cards and buttons and runs an SVG filter on just those outlines, so they read as scribbled by hand rather
That's the crayon-box feel you can't fake with a font — the friendly kids'-studio or handmade-shop look you screenshot on Pinterest and then can't find the name for. Below is a full one-page landing built with it, live, not a mockup. Scroll it top to bottom once before we break it into parts.

Now the same page taken apart into nine reusable pieces, laid out in the order a visitor's eye actually moves through a landing — orient, browse features, check price, trust, act, sign up. Three of them (the scribble hero, the double-line cards, the speech-bubble testimonial) share one trick: the wobble filter sits on a separate SVG layer that draws only the border, never the one wrapping the text.
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
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Crayon-scribble hero
- 02 Hand-drawn circle nav
- 03 Wobbly sticker card grid
- 04 Hand-drawn checklist
- 05 Star-and-circle pricing
- 06 Crayon speech-bubble testimonial
- 07 Crayon-hatch CTA band
- 08 Scissor-cut zigzag footer
- 09 Double-line crayon buttons & inputs
The whole kit runs on seven colors, fewer than most pencil web design palettes. Text always stays on the one ink color at the bottom of this table — that's what keeps a hand-drawn look legible. The candy-bright direction of this same design pillar is Pastel Website Design Kit, and its fruit-colored lift is Citrus Web Design Kit.
| Role | Color | Used for |
|---|---|---|
| Paper | #fff8ef |
Page background |
| Ink | #33261c |
All text (contrast 12.1–13.9:1) |
| Crayon red | #ff5a5f |
Hand-drawn strokes, accent borders |
| Crayon yellow | #ffcd3c |
Buttons, stars, underline |
| Crayon blue | #4fa3e3 |
Double lines, blooming ring accent |
| Crayon green | #6dbe6d |
Checkmarks, avatars |
| Crayon purple | #a685e2 |
CTA background tint |
01Crayon-scribble hero
What these crayon design ideas draw
The card's border is a separate SVG rect layer, and only that layer carries a feTurbulence wobble filter, so the dashed outline reads as hand-drawn while the headline stays crisp.

<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">Just like it was drawn by hand</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">A crayon-feel landing kit</p>
<button class="herocrayon__cta" type="button">Free trial</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 · Crayon-scribble hero — Measured area 16.178% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where to look in the code
The yellow underline below the title draws itself on and off with 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>
02Hand-drawn circle nav
What happens on screen
A red crayon loop circles the active tab's label — not a clean ring but a double loop where one end overshoots the other, the way a real hand circles something twice. stroke-dasharray draws it on, holds, then draws it off in reverse.

<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">Crayon</span>
<ul class="navcrayon__list">
<li class="navcrayon__item navcrayon__item--active">
<span class="navcrayon__label">Home</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">Classes</span></li>
<li class="navcrayon__item"><span class="navcrayon__label">Pricing</span></li>
<li class="navcrayon__item"><span class="navcrayon__label">Contact</span></li>
</ul>
<button class="navcrayon__cta" type="button">Book a trial</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 · Hand-drawn circle nav — Measured area 0.344% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password
How to read the code
<li class="relative px-2 py-1">
<span class="relative z-10 text-xs font-semibold">Home</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>
03Wobbly sticker card grid
What moves
Each card layers a red and a blue outline offset by 1–2px, the look of a crayon line drawn twice.

<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">Hand-drawn icons</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">Popular</span>
<span class="cardcrayon__dot"></span>
<p class="cardcrayon__label">Wobbly cards</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">New</span>
<span class="cardcrayon__dot"></span>
<p class="cardcrayon__label">Crayon badges</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">Free</span>
<span class="cardcrayon__dot"></span>
<p class="cardcrayon__label">Doodle animation</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 · Wobbly sticker card grid — Measured area 13.402% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
What to change
The four cards tilt in sequence with each one's animation delayed 140ms after the last.
<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>
04Hand-drawn checklist
What you see
Each checkmark is one SVG stroke drawing left to right, not an icon font glyph.

<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">Drag to place</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">Pick a color and paint</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">Choose a hand-drawn icon</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">Share right away</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 · Hand-drawn checklist — Measured area 0.372% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where to look in the code
Every row gets its own color and start delay, so the checks land on their own beat instead of all at once.
<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>
05Star-and-circle pricing
What happens on screen
Three plan cards each sit in a different crayon tone, and only the recommended plan's price gets a hand-drawn circle that loops itself while two small stars twinkle out of sync beside it.

<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">Sprout</p>
<p class="pricecrayon__price">Free</p>
<p class="pricecrayon__note">Forever</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">Crayon</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 / mo</p>
</span>
<p class="pricecrayon__note">Most popular</p>
</div>
<div class="pricecrayon__card">
<p class="pricecrayon__name">Family</p>
<p class="pricecrayon__price">₩9,000 / mo</p>
<p class="pricecrayon__note">3+ people</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 · Star-and-circle pricing — Measured area 0.613% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
How to read the code
No table needed — color and the circle alone say which plan to pick.
.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; }
}
06Crayon speech-bubble testimonial
What moves
The quote sits inside an SVG path with a bumpy outline instead of a clean rectangle, and the tail is two crooked line segments instead of a neat triangle.

<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">It feels like our class's own website</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">Kim Dodam · Art academy director</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 · Crayon speech-bubble testimonial — Measured area 3.608% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password
What to change
The star rating fills five widths at 100/100/100/80/40% so it reads like a real, slightly-mixed review score.
<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>
07Crayon-hatch CTA band
What you see
The band's background is one repeating-linear-gradient line — no image — drawn as tight crayon hatching, and the button on top wears a dashed red frame that keeps marching, as if it's being redrawn in a loop.

<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">Color it in like a crayon, right now</h2>
<p class="ctacrayon__desc">Try it for free</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">Get started</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 · Crayon-hatch CTA band — Measured area 1.203% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
Where to look in the code
It reads like a button sitting on a page from a coloring book.
.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; } }
08Scissor-cut zigzag footer
What happens on screen
The footer's top edge is one clip-path: polygon() with teeth of uneven height — not one element per notch — so it reads like a kid's rough scissor cut instead of a machine-perfect zigzag.

<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">Classes</p>
<p class="footcrayon__link">Lessons</p>
<p class="footcrayon__link">Resources</p>
</div>
<div class="footcrayon__col footcrayon__col--b">
<p class="footcrayon__head">Brand</p>
<p class="footcrayon__link">About</p>
<p class="footcrayon__link">Contact</p>
</div>
<div class="footcrayon__col footcrayon__col--c">
<p class="footcrayon__head">Help</p>
<p class="footcrayon__link">FAQ</p>
<p class="footcrayon__link">Guides</p>
</div>
<p class="footcrayon__bottom">
Crayon · One drawing a day
<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 · Scissor-cut zigzag footer — Measured area 0.346% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password
How to read the code
Uneven height at each point is what sells the illusion.
.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%
);
}
09Double-line crayon buttons & inputs
What moves
The button carries a red-and-blue double outline that drifts slightly out of register on a loop, and the input grows a loosely hand-drawn blue circle that blooms outward and fades on its own repeating cycle.

<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">Name</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="Type your name" 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">Save</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 · Double-line crayon buttons & inputs — Measured area 3.212% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password
What to change
.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); }
}
Custom tailwind.config
Keep the seven crayon colors and three radii in this one place and both the one-pager and all nine parts stay in sync. The wobble filter's id should also stay singular across a project — reuse it, don't redeclare it.
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" },
},
},
Vibe-Coding Prompt
Once the config is in your project, the instruction to Claude Code or Cursor comes down to where you do and don't attach the wobble filter. The full prompt text lives in the zip's prompt.md.
Keep the background a single paper color (#fff8ef). Every bordered box gets a
wobbly hand-drawn outline (SVG feTurbulence + feDisplacementMap) on its own
layer — never on the element wrapping the text itself. Round corners to
rounded-crayon or rounded-pill only. For double lines, offset a red and a
blue stroke by 1-2px.
Here's what that prompt produced when applied to page/index.html, captured as a full 1280px screenshot.

Where it breaks (the trap)
The first real trap was where the wobble filter gets attached. Wrapping the whole card — border and headline together — in filter: url(#rough) pushed every pixel through the same displacement map, so the title text warped along with the border and got hard to read at small sizes. Pulling the border into its own SVG rect/path layer and filtering only that layer fixed it — the text stayed sharp, only the outline wobbled.
The second trap showed up in the actual motion measurement. With just two 14px stars blinking in the footer, the measured changed-pixel area came out to 0.07%, under this site's 0.08% floor — visually indistinguishable from a static frame. Growing the stars to 20px and widening the scale range from 0.7–1.2x to 0.5–1.7x pushed it to 0.35%, which passed. The third trap was the size of the circle around the active nav tab: a 92×50 viewBox let the loop spill into the next tab's label, and shrinking it to 56×30 made it hug only the intended word. How much of a crayon line survives inside a mail app was re-measured in Marketing Email Design. All three fixes are in the zip behind the password r6qyfm69.
All nine demos detect MDN's prefers-reduced-motion media query and stop the line-draw and card-tilt animations, freezing every element in its finished state — a completed checkmark, a closed circle — instead of hiding it.
Accessibility
The most common way a hand-drawn kit fails accessibility is running the wobble filter on the text itself. feDisplacementMap pushes every pixel of the source graphic by the noise map's value, letter strokes included, and at small sizes that blurs glyphs into illegibility. This kit keeps the filter on a border-only SVG layer at all times and never touches text elements. Text stays on one dark ink (#33261c), reaching 13.9:1 against the paper background and 12.1:1 against the darkest tint in the kit (the purple card tint) — well past the WCAG 2.1 minimum contrast of 4.5:1. The red and blue accent strokes are decorative borders and aren't held to text contrast rules, but keyboard focus on every button and input is drawn with its own :focus-visible outline, not left to depend on a colored line.
If pastel is more your brand's speed than crayon, the pastel candy landing kit covers that ground, and the full design trends category and this site's about page round out where else to look.
FAQ
How is crayon web design different from just using a sketch font?
A sketch font reshapes the letters themselves to look handwritten; this kit keeps letters in a clean typeface and only wobbles the decorative lines — borders, underlines, checkmarks — with an SVG filter. Wobbling text hurts readability; wobbling only the border keeps the charm without losing it. The kit that answers the same trust slots with a clean-cut direction is SaaS Landing Page Design.
Can this cute web design style work for a dark-themed brand?
Yes, but swapping paper (#fff8ef) for a dark background means flipping the ink text color to something light too, or contrast collapses. The saturated crayon reds and yellows still read fine on dark backgrounds; the card tints need a darker version of themselves to stay in balance.
Is the feTurbulence filter slow to render?
It's applied to one small SVG layer per border and its values are fixed, so it isn't recomputed every frame — the motion you see comes from stroke-dashoffset and transform, not from re-running the filter. Filtering a whole card or the full page would be the slow version; keep it scoped to small border layers like this kit does.









