02ゼリーインジケーターナビ
アクティブなタブを示すミント色の pill が隣へ移動するとき横に伸び、到着するとぷにっと縮みます。ゼリーをつまんだときのあの弾力です。
使いどころLP上部バー・アプリ内タブ切替
scaleY 스쿼시translateX 25%radius pill

<main class="stage">
<nav class="navcandy" aria-label="メインメニュー">
<span class="navcandy__brand">プリン</span>
<div class="navcandy__track">
<span class="navcandy__pill" aria-hidden="true"></span>
<span class="navcandy__tab navcandy__tab--on">ホーム</span>
<span class="navcandy__tab">機能</span>
<span class="navcandy__tab">料金プラン</span>
<span class="navcandy__tab">お問い合わせ</span>
</div>
<button class="navcandy__cta" type="button">はじめる</button>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 알약이 네 칸을 한 바퀴 도는 주기
$easing: $ease-spring; // 도착할 때 통통하게 줄어드는 곡선
$color: $stage-ink; // 글자 기본(토큰) — 아래 베리 잉크로 덮는다
// 캔디 팔레트 — 값은 tailwind.config.js · tokens.css 와 같다
$candy-cream: #fff6f0;
$candy-bubble: #ffb9cf;
$candy-lavender: #cdbcf2;
$candy-mint: #b5e8d2;
$candy-berry: #55395e;
// 무대 — 라벤더 물색
@include stage(mix($candy-lavender, $stage-paper, 30%));
.navcandy {
box-sizing: border-box;
width: clamp(280px, 92vw, 440px);
display: flex;
align-items: center;
gap: $sp-2;
padding: $sp-2;
border-radius: $r-pill;
background: $stage-paper;
box-shadow: 0 8px 20px rgba($candy-lavender, .55);
}
.navcandy__brand {
flex: 0 0 auto;
padding-left: $sp-3;
color: $candy-berry;
font-weight: 700; font-size: clamp(11px, 2.6vw, 13px); line-height: 1;
}
.navcandy__track {
position: relative;
flex: 1 1 auto;
min-width: 0;
display: flex;
border-radius: $r-pill;
background: mix($candy-cream, $stage-paper, 60%);
}
.navcandy__pill {
position: absolute;
top: $sp-1;
bottom: $sp-1;
left: $sp-1;
width: calc(25% - $sp-1);
box-sizing: border-box;
border-radius: $r-pill;
background: $candy-mint;
box-shadow: 0 4px 10px rgba($candy-mint, .9);
// 네 칸을 순서대로 옮겨 다니며 도착할 때마다 젤리처럼 세로로 살짝 눌린다
animation: pillWalk $duration $easing infinite;
}
.navcandy__tab {
position: relative;
z-index: 1;
flex: 1 1 auto;
min-width: 0;
padding: $sp-2 0;
text-align: center;
color: rgba($candy-berry, .78);
font-weight: 600; font-size: clamp(10px, 2.4vw, 12px); line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.navcandy__tab--on {
color: $candy-berry;
font-weight: 700;
}
.navcandy__cta {
flex: 0 0 auto;
border: 0;
border-radius: $r-pill;
background: $candy-bubble;
color: $candy-berry;
padding: $sp-2 $sp-4;
font-weight: 700; font-size: clamp(10px, 2.4vw, 12px); line-height: 1;
box-shadow: 0 5px 12px rgba($candy-bubble, .7);
}
.navcandy__cta:focus-visible {
outline: 3px solid $candy-berry;
outline-offset: 2px;
}
// 데모: 칸 폭은 25% — translateX(100%) 가 정확히 한 칸. 도착 순간 scaleY 로 눌렸다 펴진다
// 데모: % 이동은 이 요소 자기 폭 기준 — 폭이 calc(25% - 4px) 라 한 칸(트랙의 25%)보다 4px 모자라
// 이동량을 calc(100% + 4px) 씩 보정한다(보정 없으면 세 칸을 지나며 12px 밀린다 — 실측)
@keyframes pillWalk {
0%, 24% { transform: translateX(0) scaleY(1); }
30% { transform: translateX(0) scaleY(.82); }
36%, 49% { transform: translateX(calc(100% + 4px)) scaleY(1); }
55% { transform: translateX(calc(100% + 4px)) scaleY(.82); }
61%, 74% { transform: translateX(calc(200% + 8px)) scaleY(1); }
80% { transform: translateX(calc(200% + 8px)) scaleY(.82); }
86%, 100% { transform: translateX(calc(300% + 12px)) scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
.navcandy__pill { 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: rgb(94.1176470588%, 92.1176470588%, 98.4705882353%);
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.navcandy {
box-sizing: border-box;
width: clamp(280px, 92vw, 440px);
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 999px;
background: #ffffff;
box-shadow: 0 8px 20px rgba(205, 188, 242, 0.55);
}
.navcandy__brand {
flex: 0 0 auto;
padding-left: 12px;
color: #55395e;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
.navcandy__track {
position: relative;
flex: 1 1 auto;
min-width: 0;
display: flex;
border-radius: 999px;
background: rgb(100%, 97.8823529412%, 96.4705882353%);
}
.navcandy__pill {
position: absolute;
top: 4px;
bottom: 4px;
left: 4px;
width: calc(25% - 4px);
box-sizing: border-box;
border-radius: 999px;
background: #b5e8d2;
box-shadow: 0 4px 10px rgba(181, 232, 210, 0.9);
animation: pillWalk 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.navcandy__tab {
position: relative;
z-index: 1;
flex: 1 1 auto;
min-width: 0;
padding: 8px 0;
text-align: center;
color: rgba(85, 57, 94, 0.78);
font-weight: 600;
font-size: clamp(10px, 2.4vw, 12px);
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.navcandy__tab--on {
color: #55395e;
font-weight: 700;
}
.navcandy__cta {
flex: 0 0 auto;
border: 0;
border-radius: 999px;
background: #ffb9cf;
color: #55395e;
padding: 8px 16px;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 12px);
line-height: 1;
box-shadow: 0 5px 12px rgba(255, 185, 207, 0.7);
}
.navcandy__cta:focus-visible {
outline: 3px solid #55395e;
outline-offset: 2px;
}
@keyframes pillWalk {
0%, 24% {
transform: translateX(0) scaleY(1);
}
30% {
transform: translateX(0) scaleY(0.82);
}
36%, 49% {
transform: translateX(calc(100% + 4px)) scaleY(1);
}
55% {
transform: translateX(calc(100% + 4px)) scaleY(0.82);
}
61%, 74% {
transform: translateX(calc(200% + 8px)) scaleY(1);
}
80% {
transform: translateX(calc(200% + 8px)) scaleY(0.82);
}
86%, 100% {
transform: translateX(calc(300% + 12px)) scaleY(1);
}
}
@media (prefers-reduced-motion: reduce) {
.navcandy__pill {
animation: none !important;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./NavCandy.module.scss";
export type NavCandyProps = {
brand?: string;
tabs?: string[];
cta?: string;
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function NavCandy({
brand = "푸딩",
tabs = ["홈", "기능", "요금제", "문의"],
cta = "시작하기",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#b5e8d2",
}: NavCandyProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<nav className={styles.navcandy} aria-label="주요 메뉴" style={vars}>
<span className={styles.brand}>{brand}</span>
<div className={styles.track}>
<span className={styles.pill} aria-hidden="true" />
{tabs.map((t, i) => (
<span key={t} className={`${styles.tab} ${i === 0 ? styles.tabOn : ""}`}>{t}</span>
))}
</div>
<button type="button" className={styles.cta}>{cta}</button>
</nav>
);
}02 ゼリーインジケーターナビ — 計測 累積面積 5.476% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パステル デザイン LPキット、キャンディカラーのセクション9選
このデモが属する記事
パステル デザイン LPキット、キャンディカラーのセクション9選パステル デザインは、クリーム色の地にバブルガムピンク・ピーチ・レモン・ミント・ラベンダーの淡い色を重ね、影までグレーではなく同系色のパステルで描いて、飴のようにやわらかく、さわやかに見せるスタイルです。名前がなくて検索できなかった、あのデ この記事のデモをすべて見る


