Scrollspy 탐색 빌드

스크롤스파이 탐색은 현재 표시 영역에 있는 페이지 섹션에 해당하는 탐색 링크를 자동으로 강조 표시하는 일반적인 UI 패턴입니다. 이렇게 하면 사용자가 긴 문서를 스크롤할 때 현재 위치를 명확하게 파악할 수 있습니다.

이 패턴은 다음 두 가지 시나리오에서 일반적입니다.

  • 단일 페이지 웹사이트: 기본 사이트 탐색이 별도의 URL이 아닌 단일 페이지의 여러 섹션으로 연결되는 경우입니다.
  • 긴 형식 콘텐츠: 목차 또는 탐색 사이드바에서 기사, 가이드 또는 문서의 사용자 진행 상황을 추적하는 경우

구현 방법

기존에는 스크롤스파이 패턴에 JavaScript와 IntersectionObserver API가 필요했습니다. 최신 CSS는 scroll-target-group 속성과 :target-current 가상 클래스를 사용하여 이를 달성하는 네이티브 방법을 제공합니다.

  1. 탐색 링크에 프래그먼트 식별자 사용: 탐색은 콘텐츠 섹션의 id와 일치하는 프래그먼트 식별자(예: <a href="#section-1">)를 사용하여 동일한 페이지의 섹션을 가리키는 앵커 링크(<a>)로 구성되어야 합니다.
  2. 스크롤 타겟 그룹화 사용 설정: 탐색 링크의 컨테이너에 scroll-target-group: auto 속성을 적용합니다. 이렇게 하면 브라우저가 이러한 링크를 각 타겟을 추적하는 마커로 처리합니다.
  3. 활성 링크 스타일 지정: 탐색 링크에 :target-current 가상 클래스를 사용하여 스크롤 컨테이너에 타겟 섹션이 표시될 때 스타일을 적용합니다.
  4. 시맨틱 상태: 스크롤이 중지될 때 활성 링크의 aria-current="true"를 업데이트하는 스크립트를 사용하여 상태가 보조 기술에 올바르게 전달되도록 합니다.

예시 코드

이 예에서는 사용자가 섹션을 스크롤할 때 고정된 탐색 메뉴에서 활성 링크를 강조 표시합니다.


/* MANDATORY: The container of markers must have scroll-target-group: auto */
nav {
  /* Enables automatic tracking of fragment links */
  scroll-target-group: auto;
}
/* MANDATORY: Use :target-current to style the link whose target is in view. */
nav a:target-current {
  color: var(--accent, #007bff);
  border-bottom-color: var(--accent, #007bff);
}
<nav>
  <a href="#intro">Introduction</a>
  <a href="#features">Features</a>
  <a href="#pricing">Pricing</a>
</nav>

<main>
  <!-- MANDATORY: Each section must have a unique ID matching a link fragment in the <nav>. -->
  <section id="intro">
    <h2>Introduction</h2>
    <!-- content -->
  </section>
  <section id="features">
    <h2>Features</h2>
    <!-- content -->
  </section>
  <section id="pricing">
    <h2>Pricing</h2>
    <!-- content -->
  </section>
</main>

<script type="module">
  if (CSS.supports('scroll-target-group: auto')) {
    const syncAriaCurrent = () => {
      const currentLink = document.querySelector('nav a:target-current');
      document.querySelectorAll('nav a').forEach(link => {
        link.setAttribute('aria-current', link === currentLink ? 'true' : 'false');
      });
    };
    // Update the aria on initial page load. Ensure the DOM content is loaded, either by using `<script type="module">` or an event listener for `DOMContentLoaded`.
    syncAriaCurrent();
    // Update the aria after scroll ends
    document.addEventListener('scrollend', syncAriaCurrent);
  }
</script>

권장사항

  • 탐색 링크 전용 컨테이너에 scroll-target-group: auto를 사용합니다. 이렇게 하면 브라우저가 추적해야 하는 링크를 알 수 있습니다.
  • 권장사항: 타겟에 링크의 href 속성과 일치하는 고유한 id 속성이 있는지 확인합니다.
  • 권장사항: 섹션에 충분한 세로 공간을 제공합니다. scroll-target-group는 섹션이 한 번에 하나 또는 두 개만 표시될 정도로 충분히 큰 경우 표시되는 타겟을 가장 명확하게 식별합니다.
  • 필수: 접근성을 위해 시각적 상태를 aria-current로 미러링해야 합니다. :target-current는 시각적 측면을 처리하지만 스크린 리더에는 노출되지 않습니다.
  • 활성 상태를 색상에만 의존하지 AVOID. 글꼴 두께, 밑줄, 표시기 점과 같은 다른 시각적 단서를 포함하거나 색상 간에 충분한 대비를 두어 색각 이상이 있는 사용자가 차이를 인식할 수 있도록 하세요.

대체 전략

scroll-target-group를 지원하지 않는 브라우저의 경우 IntersectionObserver API를 기반으로 하는 대체 기능을 사용해야 합니다.

<script>
  // Feature detection for scroll-target-group
  const supportsScrollTargetGroup = CSS.supports('scroll-target-group: auto');

  if (!supportsScrollTargetGroup) {
    const navLinks = document.querySelectorAll('nav a');
    const sections = document.querySelectorAll('main section');

    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const id = entry.target.getAttribute('id');
          navLinks.forEach(link => {
            const isActive = link.getAttribute('href') === `#${id}`;
            // Use the escaped colon class for consistent styling
            link.classList.toggle('\:target-current', isActive);
            link.setAttribute('aria-current', isActive);
          });
        }
      });
    }, { rootMargin: '-50% 0px -50% 0px', threshold: 0 });

    sections.forEach(section => observer.observe(section));
  }
</script>

<style>
  /* Fallback style if :target-current is not supported.
     Use :is() to combine the native pseudo-class with a fallback class for older browsers. DO NOT use `nav a:target-current, nav a.\:target-current, as the unrecognized pseudoselector invalidates the entire rule in older browsers. */
  nav a:is(:target-current, .\:target-current) {
    color: var(--accent, #007bff);
    border-bottom-color: var(--accent, #007bff);
    font-weight: bold;
  }
</style>