@meadown/scroll-progress-trigger
v1.1.7
Published
A scroll progress bar trigger for React/Next.js
Downloads
76
Maintainers
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-triggerQuick 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:
- Collects your first 5 scroll interactions
- Analyzes your expected vs actual behavior patterns
- Automatically adapts to match your natural scrolling preference
- 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: 5creates 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
- Activation: Hover or touch the trigger element to activate progress tracking
- Direction Detection: Learns your scroll preference automatically (when using 'system' mode)
- Progress Control: Scroll in your natural direction to control progress
- Block Navigation: When using
totalBlocks, progress is divided into discrete sections - Hold Behavior: Progress either holds at current value or auto-decreases based on
isAutoHoldProgress - Completion: When progress reaches 100%, the
onCompletecallback fires - Reset: Progress can be reset by scrolling in reverse or calling
resetProgress()
License
MIT � Dewan Meadown
