Hone logo
Hone
Problems

Vue Scroll Spy: Advanced Scroll Navigation

This challenge focuses on implementing a "scroll spy" feature in a Vue.js application using TypeScript. A scroll spy is a common UI pattern where navigation links dynamically highlight based on the user's current scroll position within the page. This enhances user experience by visually indicating which section of the content is currently in view.

Problem Description

Your task is to create a Vue component that implements a scroll spy functionality. This component should:

  1. Track Scroll Position: Monitor the user's scroll position as they interact with a designated scrollable container.
  2. Identify Active Section: Determine which section of content (identified by specific HTML elements) is currently most visible within the viewport.
  3. Update Navigation State: Visually indicate the corresponding navigation link associated with the active section.

Key Requirements:

  • Component Structure: Create a reusable Vue component (e.g., ScrollSpy.vue) that can be configured with target content sections and their corresponding navigation items.
  • Intersection Observer API: Utilize the IntersectionObserver API for efficient and performant scroll tracking. This avoids traditional event listener polling which can be performance-intensive.
  • Dynamic Styling: Apply an "active" class to the navigation link that corresponds to the currently visible content section.
  • Configurability: The component should accept props to define:
    • The selector for the scrollable container.
    • A collection of section identifiers (e.g., IDs or data attributes) and their associated navigation link selectors.
  • Initial State: On component mount, the initial active section should be correctly identified and its corresponding navigation link highlighted.

Expected Behavior:

When the user scrolls, the component should:

  • As a section enters the viewport, its corresponding navigation link should receive an "active" class.
  • When a new section becomes the "most visible" (e.g., its top is closest to the top of the viewport, or a significant portion is visible), the "active" class should be removed from the previous link and applied to the new one.
  • When scrolling back up, the behavior should be mirrored.

Edge Cases:

  • Multiple Sections in View: If multiple sections are partially visible, the component should have a clear logic for determining which one is considered "active" (e.g., the one whose top edge is closest to the viewport's top).
  • No Sections Visible: Handle cases where no defined sections are currently within the viewport.
  • Dynamic Content Loading: Consider how the component would behave if content sections are added or removed dynamically after initial rendering.
  • Container Scrolling vs. Window Scrolling: The component should be able to handle both the browser window as the scrollable container and a specific div element.

Examples

Let's assume a basic HTML structure for demonstration.

Example 1: Basic Setup

HTML Structure (Simplified):

<div id="app">
  <nav>
    <a href="#section-a" data-section="section-a">Section A</a>
    <a href="#section-b" data-section="section-b">Section B</a>
    <a href="#section-c" data-section="section-c">Section C</a>
  </nav>
  <div id="content-container" style="height: 300px; overflow-y: scroll;">
    <section id="section-a" style="height: 500px; background-color: lightblue;">Section A Content</section>
    <section id="section-b" style="height: 500px; background-color: lightgreen;">Section B Content</section>
    <section id="section-c" style="height: 500px; background-color: lightcoral;">Section C Content</section>
  </div>
</div>

Vue Component Props (Conceptual):

interface SectionConfig {
  sectionId: string; // e.g., "section-a"
  navLinkSelector: string; // e.g., `nav a[data-section="${sectionId}"]`
}

interface ScrollSpyProps {
  scrollContainerSelector: string; // e.g., "#content-container"
  sections: SectionConfig[];
}

Expected Behavior:

  1. Initially, "Section A" is at the top, so nav a[data-section="section-a"] gets the "active" class.
  2. As the user scrolls down and Section B becomes more prominent, nav a[data-section="section-b"] gets the "active" class, and the "active" class is removed from the Section A link.
  3. Scrolling up reverses this behavior.

Example 2: Scroll Container is the Window

HTML Structure (Simplified):

<div id="app">
  <nav>
    <a href="#intro" data-section="intro">Intro</a>
    <a href="#features" data-section="features">Features</a>
    <a href="#contact" data-section="contact">Contact</a>
  </nav>
  <main id="main-content">
    <section id="intro" style="height: 80vh; background-color: lightblue;">Introduction</section>
    <section id="features" style="height: 100vh; background-color: lightgreen;">Features</section>
    <section id="contact" style="height: 60vh; background-color: lightcoral;">Contact Us</section>
  </main>
</div>

Vue Component Props (Conceptual):

interface SectionConfig {
  sectionId: string;
  navLinkSelector: string;
}

interface ScrollSpyProps {
  scrollContainerSelector: null | string; // null indicates window scroll
  sections: SectionConfig[];
}

Expected Behavior:

When scrollContainerSelector is null (or undefined), the component should observe scroll events on the window object and apply the "active" class to the corresponding navigation links based on the visibility of sections within the browser's viewport.

Constraints

  • TypeScript: The solution must be written entirely in TypeScript.
  • Vue 3: The solution should be compatible with Vue 3 Composition API.
  • Intersection Observer: The IntersectionObserver API is the required method for scroll tracking. Avoid direct window.onscroll listeners.
  • Performance: The implementation should be performant, especially when dealing with a large number of sections or frequent scrolling.
  • Reusability: The component should be designed for reusability across different parts of an application.
  • Styling: The "active" class name can be specified as a prop or hardcoded (e.g., "active").

Notes

  • The IntersectionObserver API provides an isIntersecting property, but for scroll spy, you'll likely want to observe the top of the target element relative to the viewport or scroll container. Consider using rootMargin and threshold options to fine-tune the behavior.
  • A common strategy for determining the "active" section is to find the section whose top edge is closest to the top of the viewport (or scroll container).
  • Ensure proper cleanup of the IntersectionObserver instance when the component is unmounted to prevent memory leaks.
  • Think about how to map sectionIds to their respective navigation links. Data attributes on the navigation links are a common and flexible approach.
  • The rootMargin option of IntersectionObserver can be particularly useful here. For instance, setting a negative rootMargin like "-50% 0px" could help trigger an intersection when the center of an element is in view. For scroll spy, you might want to observe when the top of a section comes into view or is closest to the viewport's top.
Loading editor...
typescript