02巨大ワードマークナビ
ナビ左側のワードマークが、スクロールに合わせて文字サイズと字間を一緒に縮める。
使いどころLP上部バー・ブランド訴求サイト
transform-origin: left centerletter-spacing 보간sticky 헤더

<main class="stage">
<nav class="navbar is-demo">
<span class="navbar__brand">KINETIC</span>
<div class="navbar__right">
<span class="navbar__link">概要</span>
<span class="navbar__link">料金</span>
<button class="navbar__cta" type="button">始める</button>
</div>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 워드마크가 줄었다 돌아오는 한 주기
$easing: $ease-spring;
$color: $subject-ink; // 글자 색
@include stage($stage-yellow);
.navbar {
width: min(400px, 92vw);
display: flex;
align-items: center;
justify-content: space-between;
// 320px 폭에서 브랜드+링크+CTA 의 min-content 합이 92vw 를 넘겨 넘쳤다 (body.scrollWidth 실측 — gap·패딩을 뷰포트에 맞춰 줄인다
gap: clamp($sp-1, 1vw, $sp-4);
background: $subject-cream;
border-radius: $r-pill;
padding: $sp-2 $sp-2 $sp-2 clamp($sp-1, 1.6vw, $sp-4);
box-shadow: $shadow-raised;
}
// 왼쪽 워드마크만 애니메이션한다 — 스크롤에 맞춰 줄어드는 sticky 헤더를 흉내
.navbar__brand {
display: inline-flex;
transform-origin: left center; // 규칙8: 헤더 왼쪽 끝에 붙은 워드마크 — sticky 헤더처럼 좌변을 두고 줄어든다
color: $color;
font-weight: 800;
font-size: clamp(17px, 4.6vw, 23px);
line-height: 1;
letter-spacing: .01em;
animation: brand-shrink $duration $easing infinite;
}
@keyframes brand-shrink {
0%, 15% { transform: scale(1); }
50% { transform: scale(.78); }
85%, 100% { transform: scale(1); }
}
// 워드마크 글자 낱개 — index.html 말미 스크립트가 텍스트를 이 span 으로 쪼개고
// 결정론 해시로 --nx0·--ny0·--nr0(출발점) · --wx·--wy·--wr(힘장 경유점) 을 심는다. (형태2차 한 겹, 077 품질기준선)
.char {
display: inline-block;
animation: char-flow $duration $easing infinite;
}
.char--space { width: .28em; }
@keyframes char-flow {
0%, 4% { transform: translate(var(--nx0, 0), var(--ny0, 14px)) rotate(var(--nr0, 0deg)) scale(.55); opacity: 0; }
16% { transform: translate(var(--wx, 0), var(--wy, 4px)) rotate(var(--wr, 0deg)) scale(1.08); opacity: 1; }
30%, 78% { transform: translate(0, 0) rotate(0deg) scale(1); opacity: 1; }
96%, 100% { transform: translate(var(--nx0, 0), var(--ny0, 14px)) rotate(var(--nr0, 0deg)) scale(.55); opacity: 0; }
}
.navbar__right {
display: flex;
align-items: center;
gap: clamp($sp-1, 1.2vw, $sp-4);
}
.navbar__link {
color: $color;
opacity: .68;
font-weight: 600;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
.navbar__cta {
border: 0;
border-radius: $r-pill;
background: $color;
color: $subject-cream;
padding: $sp-2 clamp($sp-1, 1.6vw, $sp-4);
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
@media (prefers-reduced-motion: reduce) {
.navbar__brand { animation: none; transform: none; }
.char { animation: none; transform: none; opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.navbar {
width: min(400px, 92vw);
display: flex;
align-items: center;
justify-content: space-between;
gap: clamp(4px, 1vw, 16px);
background: #fff7e6;
border-radius: 999px;
padding: 8px 8px 8px clamp(4px, 1.6vw, 16px);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.navbar__brand {
display: inline-flex;
transform-origin: left center;
color: #17141a;
font-weight: 800;
font-size: clamp(17px, 4.6vw, 23px);
line-height: 1;
letter-spacing: 0.01em;
animation: brand-shrink 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes brand-shrink {
0%, 15% {
transform: scale(1);
}
50% {
transform: scale(0.78);
}
85%, 100% {
transform: scale(1);
}
}
.char {
display: inline-block;
animation: char-flow 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.char--space {
width: 0.28em;
}
@keyframes char-flow {
0%, 4% {
transform: translate(var(--nx0, 0), var(--ny0, 14px)) rotate(var(--nr0, 0deg)) scale(0.55);
opacity: 0;
}
16% {
transform: translate(var(--wx, 0), var(--wy, 4px)) rotate(var(--wr, 0deg)) scale(1.08);
opacity: 1;
}
30%, 78% {
transform: translate(0, 0) rotate(0deg) scale(1);
opacity: 1;
}
96%, 100% {
transform: translate(var(--nx0, 0), var(--ny0, 14px)) rotate(var(--nr0, 0deg)) scale(0.55);
opacity: 0;
}
}
.navbar__right {
display: flex;
align-items: center;
gap: clamp(4px, 1.2vw, 16px);
}
.navbar__link {
color: #17141a;
opacity: 0.68;
font-weight: 600;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
.navbar__cta {
border: 0;
border-radius: 999px;
background: #17141a;
color: #fff7e6;
padding: 8px clamp(4px, 1.6vw, 16px);
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
@media (prefers-reduced-motion: reduce) {
.navbar__brand {
animation: none;
transform: none;
}
.char {
animation: none;
transform: none;
opacity: 1;
}
}// 워드마크 글자별 노이즈 오프셋 + 벡터필드 진입 (형태2차) — KINETIC 7글자를 span 으로 쪼개고
// 결정론 해시로 출발점/경유점 변수만 심는다. 프레임은 style.scss 의 char-flow 키프레임이 つくる。.
// 기존 brand-shrink(컨테이너 스케일) 와 겹쳐 — 글자는 흩어졌다 힘장을 따라 모이고, 그 위에 워드마크 전체가 줄었다 커진다.
(function () {
function hash(n) { // 정수 → 0..1 결정론 난수 (sin 해시)
var s = Math.sin(n * 127.1 + 311.7) * 43758.5453;
return s - Math.floor(s);
}
document.querySelectorAll('.navbar .navbar__brand').forEach(function (brand) {
var text = brand.textContent;
brand.textContent = '';
for (var c = 0; c < text.length; c++) {
var ch = document.createElement('span');
if (text[c] === ' ') { ch.className = 'char char--space'; }
else {
ch.className = 'char';
ch.textContent = text[c];
var s = c * 13 + 5;
var h1 = hash(s), h2 = hash(s + 1), h3 = hash(s + 2), h4 = hash(s + 3);
var ang = h1 * Math.PI * 2;
var rad = 10 + h2 * 18;
var x0 = Math.cos(ang) * rad, y0 = 14 + h3 * 12;
var rot0 = (h4 - .5) * 30;
var wx = x0 - 0.9 * y0 * 0.55, wy = y0 + 0.9 * x0 * 0.55;
var wr = rot0 * 0.4;
ch.style.setProperty('--nx0', x0.toFixed(1) + 'px');
ch.style.setProperty('--ny0', y0.toFixed(1) + 'px');
ch.style.setProperty('--nr0', rot0.toFixed(1) + 'deg');
ch.style.setProperty('--wx', (wx * .6).toFixed(1) + 'px');
ch.style.setProperty('--wy', (wy * .18 + 3).toFixed(1) + 'px');
ch.style.setProperty('--wr', wr.toFixed(1) + 'deg');
ch.style.animationDelay = (-0.05 * c).toFixed(3) + 's';
}
brand.appendChild(ch);
}
});
})();import type { CSSProperties } from "react";
import styles from "./KineticNav.module.scss";
export type KineticNavProps = {
brand?: string;
link1?: string;
link2?: string;
cta?: string;
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type CharVars = CSSProperties & {
"--nx0"?: string; "--ny0"?: string; "--nr0"?: string;
"--wx"?: string; "--wy"?: string; "--wr"?: string;
};
/** 정수 → 0..1 결정론 난수 (sin 해시 — vanilla 02 와 동일 계열) */
function hash(n: number): number {
const s = Math.sin(n * 127.1 + 311.7) * 43758.5453;
return s - Math.floor(s);
}
/** 한 글자 = 출발점(노이즈) + 힘장 경유점 변수 묶음. vanilla index.html 스크립트와 같은 식. */
function charVars(c: number): CharVars {
const s = c * 13 + 5;
const h1 = hash(s), h2 = hash(s + 1), h3 = hash(s + 2), h4 = hash(s + 3);
const ang = h1 * Math.PI * 2;
const rad = 10 + h2 * 18;
const x0 = Math.cos(ang) * rad, y0 = 14 + h3 * 12;
const rot0 = (h4 - .5) * 30;
// 회전 힘장 v(p) = ω × p 를 출발점에서 dt 적분 — v = ω*(-y, x), dt=0.55, ω=0.9
const wx = x0 - 0.9 * y0 * 0.55, wy = y0 + 0.9 * x0 * 0.55;
const wr = rot0 * 0.4;
return {
"--nx0": `${x0.toFixed(1)}px`,
"--ny0": `${y0.toFixed(1)}px`,
"--nr0": `${rot0.toFixed(1)}deg`,
"--wx": `${(wx * .6).toFixed(1)}px`,
"--wy": `${(wy * .18 + 3).toFixed(1)}px`,
"--wr": `${wr.toFixed(1)}deg`,
animationDelay: `${(-0.05 * c).toFixed(3)}s`,
};
}
export default function KineticNav({
brand = "KINETIC",
link1 = "About",
link2 = "Pricing",
cta = "Get started",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#17141a",
}: KineticNavProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.stage} style={vars}>
<nav className={styles.navbar}>
<span className={styles.brand}>
{brand.split("").map((ch, c) =>
ch === " " ? (
<span key={c} className={styles.charSpace} />
) : (
<span key={c} className={styles.char} style={charVars(c)}>{ch}</span>
)
)}
</span>
<div className={styles.right}>
<span className={styles.link}>{link1}</span>
<span className={styles.link}>{link2}</span>
<button type="button" className={styles.cta}>{cta}</button>
</div>
</nav>
</div>
);
}02 巨大ワードマークナビ — 計測 累積面積 1.86% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · キネティックタイポグラフィをコードで分解してみた
このデモが属する記事
キネティックタイポグラフィをコードで分解してみたキネティックタイポグラフィとは、見出しの文字サイズが画面幅に合わせて変わり、単語が順番に浮かび上がり、数字がオドメーターのように上がっていくデザイン手法のことです。最近のランディングページはどれもファーストビューがこれなので、実際にコードを この記事のデモをすべて見る


