npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@meadown/scroll-progress-trigger

v1.1.7

Published

A scroll progress bar trigger for React/Next.js

Downloads

76

Readme

@meadown/scroll-progress-trigger

A powerful, lightweight React/Next.js library for creating scroll-controlled experiences with precise block-based navigation. Perfect for image galleries, story viewers, product tours, and any scroll-driven UI.

Features

  • 🎯 Trigger-based activation - Progress only advances when hovering/touching specific elements
  • 📱 Touch & Mouse support - Works seamlessly on desktop and mobile devices
  • 🧠 Smart scroll direction - Automatically adapts to user's natural scroll preferences
  • 🎛️ Block-based navigation - Navigate arrays without skipping items - solved the common "one scroll jumps multiple items" problem
  • 🔄 Smooth boundaries - Never get stuck at 0% or 100%, seamlessly transition between sections
  • ⏸️ Flexible hold behavior - Choose between holding progress or auto-decreasing when idle
  • 🪶 Lightweight - Zero dependencies (except React), efficient implementation
  • 🎨 Fully customizable - Complete control over behavior and styling
  • 📝 Full TypeScript support - Complete type definitions with IntelliSense support

Installation

npm install @meadown/scroll-progress-trigger

Quick Start

Basic Progress Bar

import { useRef } from "react"
import { useScrollProgress } from "@meadown/scroll-progress-trigger"

function ProgressBar() {
  const triggerRef = useRef<HTMLDivElement>(null)

  const { progress, isActive } = useScrollProgress({
    triggerRef,
    scrollDuration: 3000,
    onComplete: () => console.log("Completed!")
  })

  return (
    <div ref={triggerRef} style={{ padding: "20px" }}>
      <div style={{
        width: "100%",
        height: "4px",
        background: "#f0f0f0",
        borderRadius: "2px"
      }}>
        <div style={{
          width: `${progress}%`,
          height: "100%",
          background: "#3b82f6",
          transition: "width 0.1s ease-out"
        }} />
      </div>
      <p>{isActive ? "Scrolling..." : "Hover and scroll"} - {progress}%</p>
    </div>
  )
}

Array Navigation (Solving the "Skipping Items" Problem)

Problem: With standard progress bars, one scroll event can jump multiple array items. Solution: Use totalBlocks with scrollsPerBlock for precise control.

import { useRef } from "react"
import { useScrollProgress } from "@meadown/scroll-progress-trigger"

function ImageGallery() {
  const triggerRef = useRef<HTMLDivElement>(null)
  const images = [
    "photo1.jpg",
    "photo2.jpg",
    "photo3.jpg",
    "photo4.jpg",
    "photo5.jpg"
  ]

  const { currentIndex, blockProgress } = useScrollProgress({
    triggerRef,
    totalBlocks: images.length,
    scrollsPerBlock: 3, // Requires 3 scroll events to move to next image
    onIndexChange: (current, previous) => {
      console.log(`Changed from image ${previous} to ${current}`)
    }
  })

  return (
    <div ref={triggerRef} style={{ position: "relative", height: "500px" }}>
      <img
        src={images[currentIndex!]}
        alt={`Photo ${currentIndex! + 1}`}
        style={{
          width: "100%",
          height: "100%",
          objectFit: "cover",
          opacity: (100 - blockProgress!) / 100 + 0.5 // Fade effect
        }}
      />
      <div style={{ position: "absolute", bottom: 20, left: 20, color: "white" }}>
        Image {currentIndex! + 1} of {images.length}
      </div>
    </div>
  )
}

API

useScrollProgress Options

| Option | Type | Default | Description | | -------------------- | --------------------------------------------- | ----------- | ------------------------------------------------------ | | onComplete | () => void | undefined | Callback fired when progress reaches 100% | | scrollDuration | number | 3000 | Duration in milliseconds for full progress | | triggerRef | React.MutableRefObject<HTMLElement \| null> | undefined | Element that triggers progress on hover/touch | | scrollDirection | 'natural' \| 'inverted' \| 'system' | 'system' | Scroll direction behavior (auto-detects when 'system') | | isAutoHoldProgress | boolean | true | Whether progress holds when not scrolling vs auto-decreases | | totalBlocks | number | undefined | Divide progress into discrete blocks/indices | | scrollsPerBlock | number | 1 | Number of scroll events required to advance one block | | snapToBlocks | boolean | false | Snap progress to exact block boundaries | | onIndexChange | (current: number, previous: number) => void | undefined | Callback fired when block index changes | | onBlockProgress | (index: number, progress: number) => void | undefined | Callback fired on scroll with current block progress |

Return Values

| Value | Type | Description | | --------------- | ------------ | --------------------------------------------- | | progress | number | Current progress value (0-100) | | resetProgress | () => void | Function to reset progress to 0 | | isActive | boolean | Whether scroll progress is currently active | | currentIndex | number | Current block index (when totalBlocks is set) | | previousIndex | number | Previous block index (when totalBlocks is set)| | blockProgress | number | Progress within current block 0-100 |

Scroll Direction Behavior

The library automatically adapts to your natural scroll preferences:

  • 'system' (default): Automatically detects your scroll direction preference within the first 5 scroll interactions
  • 'natural': Scroll down = increase progress (like iOS/macOS natural scrolling)
  • 'inverted': Scroll down = decrease progress (traditional scrolling)

Auto-Detection Process

When using scrollDirection: 'system', the library:

  1. Collects your first 5 scroll interactions
  2. Analyzes your expected vs actual behavior patterns
  3. Automatically adapts to match your natural scrolling preference
  4. Falls back to device heuristics if needed
const { progress } = useScrollProgress({
  scrollDirection: "system" // Auto-detects your preference
  // scrollDirection: 'natural', // Force natural scrolling
  // scrollDirection: 'inverted', // Force traditional scrolling
})

Progress Hold Behavior

Control what happens when you stop scrolling:

Auto-Hold Progress (default)

  • isAutoHoldProgress: true: Progress stays at current value when you stop scrolling
  • Perfect for scenarios where you want users to maintain their progress position
const { progress } = useScrollProgress({
  isAutoHoldProgress: true, // Progress stays at 50% if you stop scrolling at 50%
})

Auto-Decrease Progress

  • isAutoHoldProgress: false: Progress automatically decreases toward 0 when you stop scrolling
  • Ideal for creating urgency or automatic reset behavior
const { progress } = useScrollProgress({
  isAutoHoldProgress: false, // Progress drops from 50% → 0 when you stop scrolling
})

Auto-Decrease Details

When isAutoHoldProgress: false:

  • ⏱️ 500ms delay before auto-decrease starts
  • 📉 Smooth animation at the same rate as scroll interaction
  • 🛑 Instant interruption when user scrolls again
  • 🎯 Only when active - only decreases while hovering/touching the trigger element

Block-Based Navigation (Array Support)

Perfect for navigating through arrays of items without skipping! This solves the common problem where one scroll event jumps multiple items.

Basic Array Navigation

import React, { useRef } from "react"
import { useScrollProgress } from "@meadown/scroll-progress-trigger"

function ImageGallery() {
  const triggerRef = useRef<HTMLDivElement>(null)
  const images = ['img1.jpg', 'img2.jpg', 'img3.jpg', 'img4.jpg', 'img5.jpg']

  const { currentIndex } = useScrollProgress({
    totalBlocks: images.length,
    scrollsPerBlock: 3, // Requires 3 scrolls to move to next image
    triggerRef,
    onIndexChange: (current, previous) => {
      console.log(`Moved from image ${previous} to ${current}`)
    }
  })

  return (
    <div ref={triggerRef}>
      <img src={images[currentIndex]} alt={`Image ${currentIndex + 1}`} />
      <p>Image {currentIndex + 1} of {images.length}</p>
    </div>
  )
}

Story/Slide Experience

const stories = [
  { title: "Intro", content: "..." },
  { title: "Chapter 1", content: "..." },
  { title: "Chapter 2", content: "..." },
  { title: "Conclusion", content: "..." }
]

const { currentIndex, blockProgress } = useScrollProgress({
  totalBlocks: stories.length,
  snapToBlocks: true, // Clean snap to each story
  scrollsPerBlock: 2,
  onIndexChange: (index) => {
    // Trigger animations when story changes
    animateStoryEntry(stories[index])
  }
})

return (
  <div ref={triggerRef}>
    <h1>{stories[currentIndex].title}</h1>
    <p>{stories[currentIndex].content}</p>
    {/* Use blockProgress for within-story animations */}
    <div style={{ opacity: blockProgress / 100 }}>
      Additional content fades in as you scroll through the story
    </div>
  </div>
)

Product Feature Tour

const features = [
  { name: "Camera", icon: "📷" },
  { name: "Battery", icon: "🔋" },
  { name: "Display", icon: "📱" },
  { name: "Performance", icon: "⚡" }
]

const { currentIndex, previousIndex } = useScrollProgress({
  totalBlocks: features.length,
  scrollsPerBlock: 3,
  onIndexChange: (current, previous) => {
    // Animate transition between features
    slideOut(features[previous])
    slideIn(features[current])
  }
})

Block Options Explained

totalBlocks: Divides the 0-100 progress into N blocks

  • totalBlocks: 5 creates blocks at [0-20, 20-40, 40-60, 60-80, 80-100]
  • Returns indices 0, 1, 2, 3, 4

scrollsPerBlock: Controls scroll sensitivity

  • scrollsPerBlock: 1 - Very sensitive, changes with every scroll (default)
  • scrollsPerBlock: 3 - Requires 3 scroll events to advance one block (recommended for arrays)
  • scrollsPerBlock: 5 - Very controlled, deliberate navigation

snapToBlocks: Snap behavior

  • false - Smooth progress within blocks (default)
  • true - Progress jumps directly from block to block, no in-between values

onIndexChange: Triggered when moving between blocks

onIndexChange: (current, previous) => {
  console.log(`Changed from block ${previous} to ${current}`)
}

onBlockProgress: Triggered on every scroll with current block info

onBlockProgress: (index, progress) => {
  console.log(`Block ${index} is ${progress}% complete`)
  // Use for animations within each block
}

How it Works

  1. Activation: Hover or touch the trigger element to activate progress tracking
  2. Direction Detection: Learns your scroll preference automatically (when using 'system' mode)
  3. Progress Control: Scroll in your natural direction to control progress
  4. Block Navigation: When using totalBlocks, progress is divided into discrete sections
  5. Hold Behavior: Progress either holds at current value or auto-decreases based on isAutoHoldProgress
  6. Completion: When progress reaches 100%, the onComplete callback fires
  7. Reset: Progress can be reset by scrolling in reverse or calling resetProgress()

License

MIT � Dewan Meadown