eunsoolib
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-spy

Progressive 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 옵션

OptionTypeDefault
rootElement | nullnull
rootMarginstring'0px 0px -50% 0px'
thresholdnumber | number[]0
activeClassstring'active'
currentAttributestring'data-current'
onChange(id, element) => void-

ScrollSpyInstance

PropertyType설명
currentIdstring | null현재 활성 섹션 ID
isNativeboolean네이티브 CSS 사용 여부
setActive(id) => void수동으로 활성 지정
refresh() => void타깃 재관찰
destroy() => void인스턴스 정리

React (@cbcruk/scroll-spy/react)

항목설명
useScrollSpynav ref + 현재 활성 ID
useScrollSpyHeadings컨테이너에서 헤딩 자동 수집 + scroll-spy 연동
useIsNativeScrollSpy네이티브 지원 여부 (SSR 안전)
useSmoothScroll앵커 클릭 시 offset 포함 부드러운 스크롤
<ScrollSpyNav>헤딩 목록 렌더링 컴포넌트
<ScrollSpy>헤딩 수집 + 렌더까지 묶은 컴포넌트

자동 id 규칙

generateTocuseScrollSpyHeadingsid가 없는 헤딩에 텍스트로 만든 id를 붙입니다.

  • 글자·숫자는 문자 체계와 관계없이 유지하고(소문자화), 나머지 연속 구간은 -로 바꿉니다: Getting Startedgetting-started, 설치 방법설치-방법
  • 문서에 이미 있는 id와 겹치면 -2, -3, … 접미사를 붙입니다: 예제, 예제-2
  • 글자·숫자가 없는 텍스트는 section(겹치면 section-2, …)을 씁니다

브라우저 지원

BrowserMode
Chrome 140+Native CSS
Edge 140+Native CSS
FirefoxJS Fallback
SafariJS Fallback

참고

Una Kravets의 CSS scroll-spy 글에서 영감을 받았습니다.

On this page