06Sub-nav dính theo
Dãy tab danh mục bên dưới header chính dính thành hàng thứ hai ngay dưới header khi bạn cuộn, cùng cố định với nhau.
Dùng choTab danh mục trang thương mại điện tử, tab mục lục trang tài liệu
top:헤더높이이중 sticky겹침 없음

<main class="stage">
<header class="head">GODRICH</header>
<div class="sentinel"></div>
<nav class="subnav is-demo">
<span class="subnav__tab is-active">Nữ</span>
<span class="subnav__tab">Nam</span>
<span class="subnav__tab">Trẻ em</span>
</nav>
<div class="page">
<p class="page__line"></p>
<p class="page__line"></p>
<p class="page__line short"></p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$head-h: 56px; // 메인 헤더 높이 — 서브 내비가 이 값 아래 붙는다
$duration: $dur-quick; // 테두리가 켜지는 시간
$color: $subject-ink; // 서브 내비 배경
// ── 데모 무대 ──────────────────────────────────────────
html, body { margin: 0; overflow: hidden; }
.stage {
min-height: 100vh;
margin: 0;
background: $stage-yellow;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.sentinel { height: 1px; }
.head {
position: sticky;
top: 0;
z-index: 2;
height: $head-h;
display: flex;
align-items: center;
padding: 0 clamp(16px, 5vw, 24px);
background: $stage-ink;
color: $subject-cream;
font-weight: 800;
font-size: 18px;
line-height: 1;
font-family: inherit;
letter-spacing: .04em;
}
// 서브 내비는 메인 헤더 바로 아래에 두 번째 줄로 붙는다 (이중 sticky, 겹침 없음)
.subnav {
position: sticky;
top: $head-h;
z-index: 1;
display: flex;
gap: 4px;
padding: clamp(6px, 3vh, 10px) clamp(16px, 5vw, 24px);
background: $color;
border-bottom: 4px solid transparent;
transition: border-color $duration $ease-out;
&.is-stuck { border-bottom-color: $subject-cream; }
&__tab {
padding: 6px 14px;
border-radius: $r-pill;
font-weight: 600;
font-size: 13px;
line-height: 1;
font-family: inherit;
color: rgba(255, 255, 255, .6);
&.is-active { color: $stage-ink; background: $subject-cream; }
}
}
// ── 데모(.is-demo): 테두리가 켜졌다 꺼졌다 2초 루프 ────────
.subnav.is-demo {
animation: stuck-loop $dur-loop $ease-out infinite;
}
@keyframes stuck-loop {
0%, 5% { border-bottom-color: transparent; }
40%, 60% { border-bottom-color: $subject-cream; }
95%, 100% { border-bottom-color: transparent; }
}
// ── 배경: 문서처럼 보이는 자리 (칸이 좁으면 줄인다) ────────────
.page { padding: clamp(8px, 4vh, 20px) 24px 0; }
.page__line {
height: 10px;
margin: 0 0 clamp(6px, 3vh, 14px);
border-radius: 4px;
background: rgba($stage-ink, .14);
&.short { width: 60%; }
}
@media (prefers-reduced-motion: reduce) {
.subnav { transition: none; }
.subnav.is-demo { animation: none !important; border-bottom-color: $subject-cream; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
min-height: 100vh;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.sentinel {
height: 1px;
}
.head {
position: sticky;
top: 0;
z-index: 2;
height: 56px;
display: flex;
align-items: center;
padding: 0 clamp(16px, 5vw, 24px);
background: #17141a;
color: #fff7e6;
font-weight: 800;
font-size: 18px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.04em;
}
.subnav {
position: sticky;
top: 56px;
z-index: 1;
display: flex;
gap: 4px;
padding: clamp(6px, 3vh, 10px) clamp(16px, 5vw, 24px);
background: #17141a;
border-bottom: 4px solid transparent;
transition: border-color 220ms ease-out;
}
.subnav.is-stuck {
border-bottom-color: #fff7e6;
}
.subnav__tab {
padding: 6px 14px;
border-radius: 999px;
font-weight: 600;
font-size: 13px;
line-height: 1;
font-family: inherit;
color: rgba(255, 255, 255, 0.6);
}
.subnav__tab.is-active {
color: #17141a;
background: #fff7e6;
}
.subnav.is-demo {
animation: stuck-loop 2s ease-out infinite;
}
@keyframes stuck-loop {
0%, 5% {
border-bottom-color: transparent;
}
40%, 60% {
border-bottom-color: #fff7e6;
}
95%, 100% {
border-bottom-color: transparent;
}
}
.page {
padding: clamp(8px, 4vh, 20px) 24px 0;
}
.page__line {
height: 10px;
margin: 0 0 clamp(6px, 3vh, 14px);
border-radius: 4px;
background: rgba(23, 20, 26, 0.14);
}
.page__line.short {
width: 60%;
}
@media (prefers-reduced-motion: reduce) {
.subnav {
transition: none;
}
.subnav.is-demo {
animation: none !important;
border-bottom-color: #fff7e6;
}
}// 실제 페이지: 서브 내비가 헤더 밑에 닿는 순간 테두리를 켠다 (IntersectionObserver, 6줄)
var sub = document.querySelector('.subnav');
new IntersectionObserver(function (es) {
sub.classList.toggle('is-stuck', !es[0].isIntersecting);
}, { rootMargin: '-56px 0px 0px 0px', threshold: 1 }).observe(document.querySelector('.sentinel'));import { useEffect, useRef } from "react";
import type { CSSProperties } from "react";
import styles from "./StickySubNav.module.scss";
export type StickySubNavProps = {
tabs: string[];
activeIndex?: number;
/** 메인 헤더 높이 (기본 56px) — 서브 내비가 이 값 아래 붙는다 */
headHeight?: number;
duration?: number;
color?: string;
};
type Vars = CSSProperties & { "--head-h"?: string; "--duration"?: string; "--color"?: string };
export default function StickySubNav({
tabs,
activeIndex = 0,
headHeight = 56,
duration = 220,
color = "#17141a",
}: StickySubNavProps) {
const nav = useRef<HTMLElement>(null);
const sentinel = useRef<HTMLDivElement>(null);
const vars: Vars = { "--head-h": `${headHeight}px`, "--duration": `${duration}ms`, "--color": color };
useEffect(() => {
if (!sentinel.current) return;
const io = new IntersectionObserver(
([e]) => nav.current?.classList.toggle("is-stuck", !e.isIntersecting),
{ rootMargin: `-${headHeight}px 0px 0px 0px`, threshold: 1 }
);
io.observe(sentinel.current);
return () => io.disconnect();
}, [headHeight]);
return (
<>
<div ref={sentinel} className={styles.sentinel} />
<nav ref={nav} className={styles.subnav} style={vars}>
{tabs.map((t, i) => (
<span key={t} className={i === activeIndex ? `${styles.tab} ${styles.active}` : styles.tab}>{t}</span>
))}
</nav>
</>
);
}06 Sub-nav dính theo — Diện tích đo được 1.333% · khung hình chuyển động 14/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 CSS sticky header không cần JS
Bài viết chứa demo này
9 CSS sticky header không cần JSCss sticky header là kiểu header dùng position: sticky để dính lại trên đầu màn hình khi bạn cuộn trang, thay vì trôi mấ Xem tất cả demo trong bài này

