02Magazine index nav
Each menu item carries a two-digit page number like a magazine index, and a tilted lemon paper chip hops one slot at a time to mark the current spot.
Best forTop navigation for editorial-toned sites
고정 폭 탭쪽번호 세리프radius 4

<main class="stage">
<nav class="ncol" aria-label="Contents">
<p class="ncol__mark">COLLAGE</p>
<div class="ncol__bar">
<span class="ncol__chip" aria-hidden="true"></span>
<ul class="ncol__tabs">
<li class="ncol__tab"><span class="ncol__no">01</span><span class="ncol__label">Cover</span></li>
<li class="ncol__tab"><span class="ncol__no">02</span><span class="ncol__label">Feature</span></li>
<li class="ncol__tab"><span class="ncol__no">03</span><span class="ncol__label">Photos</span></li>
<li class="ncol__tab"><span class="ncol__no">04</span><span class="ncol__label">Reviews</span></li>
</ul>
</div>
<p class="ncol__rule" aria-hidden="true"></p>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
// 콜라주·맥시멀 팔레트 (01 과 같은 값). 글자는 ink·slate 뿐, 레몬은 면적 전용
$cg-cream: #fbf3e4;
$cg-sheet: #ffffff;
$cg-ink: #241d2e;
$cg-slate: #57506a;
$cg-coral: #bb7f74;
$cg-sky: #668cb4;
$cg-lemon: #d1c8a3;
$serif: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
$duration: $dur-loop; // 칩이 네 칸을 왕복하는 한 주기
$easing: $ease-spring;
$color: $cg-lemon; // 현재 위치를 표시하는 종이 칩 색
@include stage($cg-cream);
.ncol {
position: relative;
width: clamp(236px, 82vw, 316px);
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-2;
}
.ncol__mark {
margin: 0;
color: $cg-ink;
font-family: $serif;
font-weight: 700;
font-size: clamp(18px, 4.6vw, 24px);
line-height: 1;
letter-spacing: .22em;
transform: rotate(-2deg);
}
.ncol__bar {
position: relative;
box-sizing: border-box;
padding: $sp-2;
border-radius: $r-xs;
background: $cg-sheet;
box-shadow: 0 5px 0 rgba($cg-ink, .16);
}
.ncol__tabs {
position: relative;
z-index: 1;
display: flex;
margin: 0;
padding: 0;
list-style: none;
}
// BD19 — 이동량이 고정 px 이므로 탭은 반드시 내용 폭이 아니라 고정 폭이어야 한다
.ncol__tab {
flex: 0 0 var(--tab);
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-1;
padding: $sp-1 0;
margin-right: $sp-1;
}
.ncol__tab:last-child { margin-right: 0; }
.ncol__no {
color: $cg-ink;
font-family: $serif;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 12px);
line-height: 1;
}
.ncol__label {
color: $cg-ink;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
// 종이 칩 — 탭 뒤에서 한 칸씩 뛴다. 칸 간격(--step) = 탭 폭 + 간격
.ncol__chip {
position: absolute;
z-index: 0;
left: $sp-2;
top: $sp-2;
width: var(--tab);
height: calc(100% - #{$sp-4});
border-radius: $r-xs;
background: $cg-lemon;
animation-name: chipHop;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.ncol__rule {
width: 100%;
height: 3px;
margin: 0;
border-radius: $r-xs;
background: repeating-linear-gradient(90deg, $cg-ink 0 12px, transparent 12px 20px);
transform: rotate(1deg);
}
.ncol { --tab: 68px; --step: 72px; }
// 데모: 칸마다 잠깐 앉았다가 다음 칸으로 뛴다. 네 칸을 갔다가 같은 길로 돌아오므로
// 끝에서 처음으로 건너뛰는 구간이 없다 — 마지막 프레임과 첫 프레임이 같은 자리다
@keyframes chipHop {
0%, 10% { transform: translateX(0) translateY(0) rotate(-3deg); }
13.3% { transform: translateX(calc(var(--step) * .5)) translateY(-11px) rotate(5deg); }
16.6%, 26.6% { transform: translateX(var(--step)) translateY(0) rotate(-3deg); }
30% { transform: translateX(calc(var(--step) * 1.5)) translateY(-11px) rotate(5deg); }
33.3%, 43.3% { transform: translateX(calc(var(--step) * 2)) translateY(0) rotate(-3deg); }
46.6% { transform: translateX(calc(var(--step) * 2.5)) translateY(-11px) rotate(5deg); }
50%, 60% { transform: translateX(calc(var(--step) * 3)) translateY(0) rotate(-3deg); }
63.3% { transform: translateX(calc(var(--step) * 2.5)) translateY(-11px) rotate(-11deg); }
66.6%, 76.6% { transform: translateX(calc(var(--step) * 2)) translateY(0) rotate(-3deg); }
80% { transform: translateX(calc(var(--step) * 1.5)) translateY(-11px) rotate(-11deg); }
83.3%, 93.3% { transform: translateX(var(--step)) translateY(0) rotate(-3deg); }
96.6% { transform: translateX(calc(var(--step) * .5)) translateY(-11px) rotate(-11deg); }
100% { transform: translateX(0) translateY(0) rotate(-3deg); }
}
@media (max-width: 360px), (max-height: 230px) {
.ncol { width: 232px; --tab: 48px; --step: 52px; gap: $sp-2; }
.ncol__mark { font-size: 15px; letter-spacing: .14em; }
.ncol__label { font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.ncol__chip { animation: none !important; transform: rotate(-3deg); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf3e4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ncol {
position: relative;
width: clamp(236px, 82vw, 316px);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.ncol__mark {
margin: 0;
color: #241d2e;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: clamp(18px, 4.6vw, 24px);
line-height: 1;
letter-spacing: 0.22em;
transform: rotate(-2deg);
}
.ncol__bar {
position: relative;
box-sizing: border-box;
padding: 8px;
border-radius: 4px;
background: #ffffff;
box-shadow: 0 5px 0 rgba(36, 29, 46, 0.16);
}
.ncol__tabs {
position: relative;
z-index: 1;
display: flex;
margin: 0;
padding: 0;
list-style: none;
}
.ncol__tab {
flex: 0 0 var(--tab);
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 4px 0;
margin-right: 4px;
}
.ncol__tab:last-child {
margin-right: 0;
}
.ncol__no {
color: #241d2e;
font-family: Georgia, "Nanum Myeongjo", "Times New Roman", serif;
font-weight: 700;
font-size: clamp(10px, 2.4vw, 12px);
line-height: 1;
}
.ncol__label {
color: #241d2e;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
.ncol__chip {
position: absolute;
z-index: 0;
left: 8px;
top: 8px;
width: var(--tab);
height: calc(100% - 16px);
border-radius: 4px;
background: #d1c8a3;
animation-name: chipHop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.ncol__rule {
width: 100%;
height: 3px;
margin: 0;
border-radius: 4px;
background: repeating-linear-gradient(90deg, #241d2e 0 12px, transparent 12px 20px);
transform: rotate(1deg);
}
.ncol {
--tab: 68px;
--step: 72px;
}
@keyframes chipHop {
0%, 10% {
transform: translateX(0) translateY(0) rotate(-3deg);
}
13.3% {
transform: translateX(calc(var(--step) * 0.5)) translateY(-11px) rotate(5deg);
}
16.6%, 26.6% {
transform: translateX(var(--step)) translateY(0) rotate(-3deg);
}
30% {
transform: translateX(calc(var(--step) * 1.5)) translateY(-11px) rotate(5deg);
}
33.3%, 43.3% {
transform: translateX(calc(var(--step) * 2)) translateY(0) rotate(-3deg);
}
46.6% {
transform: translateX(calc(var(--step) * 2.5)) translateY(-11px) rotate(5deg);
}
50%, 60% {
transform: translateX(calc(var(--step) * 3)) translateY(0) rotate(-3deg);
}
63.3% {
transform: translateX(calc(var(--step) * 2.5)) translateY(-11px) rotate(-11deg);
}
66.6%, 76.6% {
transform: translateX(calc(var(--step) * 2)) translateY(0) rotate(-3deg);
}
80% {
transform: translateX(calc(var(--step) * 1.5)) translateY(-11px) rotate(-11deg);
}
83.3%, 93.3% {
transform: translateX(var(--step)) translateY(0) rotate(-3deg);
}
96.6% {
transform: translateX(calc(var(--step) * 0.5)) translateY(-11px) rotate(-11deg);
}
100% {
transform: translateX(0) translateY(0) rotate(-3deg);
}
}
@media (max-width: 360px), (max-height: 230px) {
.ncol {
width: 232px;
--tab: 48px;
--step: 52px;
gap: 8px;
}
.ncol__mark {
font-size: 15px;
letter-spacing: 0.14em;
}
.ncol__label {
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.ncol__chip {
animation: none !important;
transform: rotate(-3deg);
}
}import type { CSSProperties } from "react";
import styles from "./NavCollage.module.scss";
export type NavCollageItem = { no: string; label: string };
export type NavCollageProps = {
mark?: string;
items?: NavCollageItem[];
/** 탭 한 칸의 고정 폭(px). 이동량이 고정 px 이라 탭도 고정 폭이어야 한다 */
tab?: number;
/** 탭 사이 간격(px) */
gap?: number;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string; "--easing"?: string; "--color"?: string;
"--tab"?: string; "--step"?: string;
};
const defaultItems: NavCollageItem[] = [
{ no: "01", label: "표지" },
{ no: "02", label: "특집" },
{ no: "03", label: "화보" },
{ no: "04", label: "후기" },
];
export default function NavCollage({
mark = "COLLAGE",
items = defaultItems,
tab = 68,
gap = 4,
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#ffd93d",
}: NavCollageProps) {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
"--tab": `${tab}px`,
"--step": `${tab + gap}px`,
};
return (
<nav className={styles.ncol} style={vars} aria-label={mark}>
<p className={styles.mark}>{mark}</p>
<div className={styles.bar}>
<span className={styles.chip} aria-hidden="true" />
<ul className={styles.tabs}>
{items.map((x) => (
<li key={x.no} className={styles.tab}>
<span className={styles.no}>{x.no}</span>
<span className={styles.label}>{x.label}</span>
</li>
))}
</ul>
</div>
<p className={styles.rule} aria-hidden="true" />
</nav>
);
}02 Magazine index nav — Measured area 8.741% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · Maximalist Web Design: 9 Collage Landing Sections
The post this demo lives in
Maximalist Web Design: 9 Collage Landing SectionsMaximalist web design fills the screen instead of emptying it, stacking color fields, oversized type, and stickers until See all demos in this post


