Lightweight React Scroll Spy Hook – Domet

Description:

Domet is a lightweight React hook that manages scroll-driven interface logic.

It handles scroll-spy functionality, progress tracking, and section awareness without heavy dependencies.

The library uses a tight scroll loop and hysteresis to track active sections accurately.

Features

  • 🧩 Headless Architecture: It exposes logic and state but leaves all styling decisions to you.
  • Performance Focused: The system uses a throttled scroll loop to prevent layout thrashing.
  • 🛡️ Hysteresis Control: It applies a score margin to stop rapid switching between sections.
  • 📱 Scroll State Access: The hook tracks global scroll velocity, direction, and progress data.

Preview

scroll-spy-hook-domet

Use Cases

  • Documentation Sites: Highlight the current chapter in a sidebar navigation as the user reads.
  • Single Page Portfolios: Trigger animations when specific project sections enter the viewport.
  • Reading Progress Bars: Update a top-aligned progress indicator based on scroll depth.
  • Lazy Loading Content: Fetch data or load heavy components only when they approach the visible area.

How to Use It

1. Add the package to your project via npm.

npm install domet

2. Import the hook and define the section IDs you want to track. The hook returns props that you spread onto your navigation buttons and content sections.

import { useDomet } from 'domet';
const sectionIds = ['home', 'features', 'pricing', 'contact'];
export default function ScrollSpyNavigation() {
  // Initialize the hook with section IDs
  const { activeId, navProps, sectionProps } = useDomet(sectionIds);
  return (
    <div className="layout">
      {/* Navigation Bar */}
      <nav className="fixed-nav">
        {sectionIds.map((id) => (
          <button
            key={id}
            {...navProps(id)}
            style={{
              fontWeight: activeId === id ? 'bold' : 'normal',
              color: activeId === id ? 'blue' : 'black',
            }}
          >
            {id.toUpperCase()}
          </button>
        ))}
      </nav>
      {/* Content Sections */}
      <main>
        {sectionIds.map((id) => (
          <section
            key={id}
            {...sectionProps(id)}
            style={{ height: '100vh', padding: '20px' }}
          >
            <h1>{id} Section</h1>
            <p>Scroll to see the navigation update.</p>
          </section>
        ))}
      </main>
    </div>
  );
}

3. You can access the scroll object to build dynamic effects based on scroll progress or velocity.

const { scroll } = useDomet(sectionIds);
// Example: A progress bar that fills as you scroll down
return (
  <>
    <div
      style={{
        position: 'fixed',
        top: 0,
        left: 0,
        height: '4px',
        background: 'red',
        width: `${scroll.progress * 100}%`,
        zIndex: 50
      }}
    />
    {/* Rest of your app */}
  </>
);

API Reference

Arguments

PropTypeDefaultDescription
sectionIdsstring[]Array of section IDs to track.
containerRefRefObject<HTMLElement> | nullnullScrollable container (defaults to window).
optionsDometOptions{}Configuration options.

Options

PropTypeDefaultDescription
offsetnumber0Trigger offset from top in pixels.
offsetRationumber0.08Viewport ratio for trigger line calculation.
debounceMsnumber10Throttle delay in milliseconds.
visibilityThresholdnumber0.6Minimum visibility ratio (0-1) for section priority.
hysteresisMarginnumber150Score margin to prevent rapid section switching.
behavior'smooth' | 'instant' | 'auto''auto'Scroll behavior.

Callbacks

PropTypeDescription
onActiveChange(id: string | null, prevId: string | null) => voidCalled when active section changes.
onSectionEnter(id: string) => voidCalled when a section enters the viewport.
onSectionLeave(id: string) => voidCalled when a section leaves the viewport.
onScrollStart() => voidCalled when scrolling starts.
onScrollEnd() => voidCalled when scrolling stops.

Return Value

PropTypeDescription
activeIdstring | nullID of the currently active section.
activeIndexnumberIndex of the active section in sectionIds (-1 if none).
scrollScrollStateGlobal scroll state (y, progress, direction, velocity).
sectionsRecord<string, SectionState>Per-section state (bounds, visibility, progress).
sectionProps(id: string) => SectionPropsProps to spread on section elements.
navProps(id: string) => NavPropsProps to spread on nav items.
registerRef(id: string) => (el: HTMLElement | null) => voidManual ref registration.
scrollToSection(id: string) => voidProgrammatically scroll to a section.

Related Resources

FAQs

Q: Does Domet work with custom scroll containers?
A: Yes. You can pass a ref pointing to your scrollable element as the second argument to useDomet.

Q: How does the library prevent flickering when sections are close together?
A: It uses a hysteresisMargin option. This adds a “stickiness” factor to the active section to prevent it from changing too quickly during minor scroll movements.

Q: Can I use this for scroll animations?
A: Yes. The hook returns a scroll object containing velocity, direction, and progress, which you can use to drive animation values.

Q: Is it compatible with Next.js?
A: Yes. Domet works in Next.js applications. Since it relies on window and DOM elements, ensure you use it within Client Components.

Add Comment