A scroll progress bar trigger for React/Next.js
npm install @meadown/scroll-progress-triggerA 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.
- π― 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
``bash`
npm install @meadown/scroll-progress-trigger
`tsx
import { useRef } from "react"
import { useScrollProgress } from "@meadown/scroll-progress-trigger"
function ProgressBar() {
const triggerRef = useRef
const { progress, isActive } = useScrollProgress({
triggerRef,
scrollDuration: 3000,
onComplete: () => console.log("Completed!")
})
return (
,
height: "100%",
background: "#3b82f6",
transition: "width 0.1s ease-out"
}} />
{isActive ? "Scrolling..." : "Hover and scroll"} - {progress}%
$3
Problem: With standard progress bars, one scroll event can jump multiple array items.
Solution: Use
totalBlocks with scrollsPerBlock for precise control.`tsx
import { useRef } from "react"
import { useScrollProgress } from "@meadown/scroll-progress-trigger"function ImageGallery() {
const triggerRef = useRef(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 (
src={images[currentIndex!]}
alt={Photo ${currentIndex! + 1}}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
opacity: (100 - blockProgress!) / 100 + 0.5 // Fade effect
}}
/>
Image {currentIndex! + 1} of {images.length}
)
}
`API
$3
| 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 | 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 |$3
| 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)$3
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
`tsx
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:
$3
- isAutoHoldProgress: true: Progress stays at current value when you stop scrolling
- Perfect for scenarios where you want users to maintain their progress position`tsx
const { progress } = useScrollProgress({
isAutoHoldProgress: true, // Progress stays at 50% if you stop scrolling at 50%
})
`$3
- isAutoHoldProgress: false: Progress automatically decreases toward 0 when you stop scrolling
- Ideal for creating urgency or automatic reset behavior`tsx
const { progress } = useScrollProgress({
isAutoHoldProgress: false, // Progress drops from 50% β 0 when you stop scrolling
})
`$3
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 elementBlock-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.
$3
`tsx
import React, { useRef } from "react"
import { useScrollProgress } from "@meadown/scroll-progress-trigger"function ImageGallery() {
const triggerRef = useRef(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 (
Image ${currentIndex + 1}} />
Image {currentIndex + 1} of {images.length}
)
}
`$3
`tsx
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 (
{stories[currentIndex].title}
{stories[currentIndex].content}
{/ Use blockProgress for within-story animations /}
Additional content fades in as you scroll through the story
)
`$3
`tsx
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])
}
})
`$3
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
`tsx
onIndexChange: (current, previous) => {
console.log(Changed from block ${previous} to ${current})
}
`
onBlockProgress: Triggered on every scroll with current block info
`tsx
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()`MIT οΏ½ Dewan Meadown