02Hand-drawn circle nav
A crayon circle loops around whichever tab is active, and every time the tab changes it redraws itself from scratch — an imperfect loop with one end poking past the other, exactly like something scribbled a second ago.
Best forLanding top bars, tab-switching UI
stroke-dashoffset 재생겹쳐 그린 원radius pill

<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 · Crayon Web Design: 9 Hand-Drawn Landing Sections
The post this demo lives in
Crayon Web Design: 9 Hand-Drawn Landing SectionsCrayon web design draws thick, wobbly outlines around cards and buttons and runs an SVG filter on just those outlines, s See all demos in this post


