DOM·CSS 브라우저 API
scroll-spy
scroll-target-group과 IntersectionObserver 폴백 기반 scroll-spy
- 🚀 네이티브 CSS
scroll-target-group+:target-current(Chrome 140+) - 🔧 미지원 브라우저를 위한 IntersectionObserver 폴백
- ⚛️ React 훅·컴포넌트 포함 (
@cbcruk/scroll-spy/react) - 📦 코어는 의존성 없음 (훅 사용 시에만 react peer 의존)
설치
pnpm add @cbcruk/scroll-spyProgressive Enhancement
CSS.supports('scroll-target-group', 'auto')로 기능을 감지해 자동으로 최적 구현을 고릅니다.
CSS.supports('scroll-target-group')
│
┌────┴────┐
✅ Yes ❌ No
│ │
네이티브 IntersectionObserver
CSS 폴백사용법
순수 CSS (Chrome 140+)
네이티브 CSS만으로도 JavaScript 없이 동작합니다.
<nav class="toc">
<a href="#intro">Introduction</a>
<a href="#features">Features</a>
</nav>
<style>
.toc {
scroll-target-group: auto;
}
.toc a:target-current {
color: var(--accent);
}
</style>Vanilla JavaScript (폴백 포함)
import { createScrollSpy } from '@cbcruk/scroll-spy'
const nav = document.querySelector('.toc') as HTMLElement
const spy = createScrollSpy(nav, {
activeClass: 'active',
rootMargin: '0px 0px -50% 0px',
onChange: (id) => console.log(`Now viewing: ${id}`),
})
console.log(spy.isNative) // Chrome 140+에서 true
spy.destroy()React 훅
import { useScrollSpyHeadings, ScrollSpyNav } from '@cbcruk/scroll-spy/react'
function Toc() {
const { headings, currentId, navRef } = useScrollSpyHeadings({
selector: 'h2, h3',
})
return <ScrollSpyNav ref={navRef} headings={headings} currentId={currentId} />
}부드러운 스크롤을 더하려면 useSmoothScroll을 조합합니다.
import { useScrollSpyHeadings, useSmoothScroll } from '@cbcruk/scroll-spy/react'
function Toc() {
const { headings, currentId, navRef } = useScrollSpyHeadings()
const { handleClick } = useSmoothScroll({ offset: 80 })
return (
<nav ref={navRef}>
{headings.map((h) => (
<a
key={h.id}
href={`#${h.id}`}
className={h.id === currentId ? 'active' : ''}
onClick={handleClick}
>
{h.text}
</a>
))}
</nav>
)
}API
코어 (@cbcruk/scroll-spy)
| 함수 | 설명 |
|---|---|
createScrollSpy(el, options?) | scroll-spy 인스턴스 생성 (네이티브/폴백 자동 선택) |
supportsScrollTargetGroup() | 네이티브 CSS 지원 여부 |
generateToc(options?) | 헤딩에서 목차 <nav> 자동 생성 (id 없는 헤딩에 id 부여) |
generateStyles(options?) | progressive enhancement용 CSS 문자열 생성 |
createScrollSpy 옵션
| Option | Type | Default |
|---|---|---|
root | Element | null | null |
rootMargin | string | '0px 0px -50% 0px' |
threshold | number | number[] | 0 |
activeClass | string | 'active' |
currentAttribute | string | 'data-current' |
onChange | (id, element) => void | - |
ScrollSpyInstance
| Property | Type | 설명 |
|---|---|---|
currentId | string | null | 현재 활성 섹션 ID |
isNative | boolean | 네이티브 CSS 사용 여부 |
setActive | (id) => void | 수동으로 활성 지정 |
refresh | () => void | 타깃 재관찰 |
destroy | () => void | 인스턴스 정리 |
React (@cbcruk/scroll-spy/react)
| 항목 | 설명 |
|---|---|
useScrollSpy | nav ref + 현재 활성 ID |
useScrollSpyHeadings | 컨테이너에서 헤딩 자동 수집 + scroll-spy 연동 |
useIsNativeScrollSpy | 네이티브 지원 여부 (SSR 안전) |
useSmoothScroll | 앵커 클릭 시 offset 포함 부드러운 스크롤 |
<ScrollSpyNav> | 헤딩 목록 렌더링 컴포넌트 |
<ScrollSpy> | 헤딩 수집 + 렌더까지 묶은 컴포넌트 |
자동 id 규칙
generateToc와 useScrollSpyHeadings는 id가 없는 헤딩에 텍스트로 만든 id를 붙입니다.
- 글자·숫자는 문자 체계와 관계없이 유지하고(소문자화), 나머지 연속 구간은
-로 바꿉니다:Getting Started→getting-started,설치 방법→설치-방법 - 문서에 이미 있는 id와 겹치면
-2,-3, … 접미사를 붙입니다:예제,예제-2 - 글자·숫자가 없는 텍스트는
section(겹치면section-2, …)을 씁니다
브라우저 지원
| Browser | Mode |
|---|---|
| Chrome 140+ | Native CSS |
| Edge 140+ | Native CSS |
| Firefox | JS Fallback |
| Safari | JS Fallback |
참고
Una Kravets의 CSS scroll-spy 글에서 영감을 받았습니다.