Skip to main content
Version: Next

useScrollProgress

The useScrollProgress hook provides an animated value that tracks scroll progress (0 to 1) for any scrollable element or the window. It's perfect for creating scroll-based animations, progress indicators, and scroll-triggered effects.

Why Use useScrollProgress?

useScrollProgress simplifies scroll progress tracking by:

  • Animated progress value - Returns a useValue-like object that animates from 0 to 1
  • Automatic calculation - Handles progress calculation based on scroll position
  • Smooth interpolation - Works seamlessly with the to function for value mapping
  • Flexible targets - Works with window, containers, or specific elements
  • Offset configuration - Control when progress starts and ends

Basic Syntax

useScrollProgress is actually useScroll called with options (instead of a callback). When you call useScroll with an options object, it returns progress values:

const { scrollYProgress, scrollXProgress } = useScroll(
refs: RefObject<HTMLElement> | Window,
options?: ScrollProgressOptions
)
Note

useScroll has two modes:

  • Callback mode: useScroll(ref, callback) - For scroll event handling
  • Progress mode: useScroll(ref, options) - For progress tracking (this page)

See useScroll for callback-based scroll handling.

Parameters

  • refs (required): Element or window to track scroll progress
  • options (optional): Configuration object for progress calculation

Return Value

Returns an object with:

  • scrollYProgress: Animated value (0-1) for vertical scroll progress
  • scrollXProgress: Animated value (0-1) for horizontal scroll progress

Options

type ScrollProgressOptions = {
target?: RefObject<HTMLElement>; // Element to track progress relative to
offset?: [string, string]; // Start and end offsets (e.g., ['start end', 'start start'])
toDescriptor?: (progress: number) => AnimationDescriptor; // Transform progress value
};

Simple Example

Track window scroll progress:

import { useScroll, animate } from 'react-ui-animate';

function ScrollProgressBar() {
const { scrollYProgress } = useScroll(window);

return (
<div style={{ height: 2000 }}>
<animate.div
style={{
position: 'fixed',
top: 0,
left: 0,
width: scrollYProgress.to([0, 1], ['0%', '100%']),
height: 4,
background: 'teal',
zIndex: 1000,
}}
/>
</div>
);
}
import React, { useRef } from 'react';
import { useScroll, animate } from 'react-ui-animate';

export default function App() {
  const { scrollYProgress } = useScroll(window);

  return (
    <div style={{ height: 3000 }}>
      <h1 style={{ padding: 40 }}>Scroll down to see progress</h1>
      
      {/* Progress Bar */}
      <animate.div
        style={{
          position: 'fixed',
          top: 0,
          left: 0,
          width: scrollYProgress.to([0, 1], ['0%', '100%']),
          height: 4,
          background: 'linear-gradient(90deg, #667eea, #764ba2)',
          zIndex: 1000,
        }}
      />

      {/* Animated Box */}
      <animate.div
        style={{
          position: 'fixed',
          top: '50%',
          left: '50%',
          translateX: -50,
          translateY: -50,
          width: 150,
          height: 150,
          scale: scrollYProgress.to([0, 1], [1, 1.5]),
          opacity: scrollYProgress.to([0, 0.5, 1], [1, 0.5, 1]),
          backgroundColor: scrollYProgress.to(
            [0, 1],
            ['#0069d9', '#ff5733']
          ),
          borderRadius: 8,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          color: 'white',
          fontWeight: 'bold',
        }}
      >
        {Math.round(scrollYProgress.get() * 100)}%
      </animate.div>

      <div style={{ height: 2000, padding: 40 }}>
        <p>Keep scrolling to see the progress change</p>
      </div>
    </div>
  );
}

Understanding Progress Values

The progress value ranges from 0 to 1:

  • 0 = Scroll position at the start (top/left)
  • 1 = Scroll position at the end (bottom/right)
  • 0.5 = Scroll position at the middle

Real-World Examples

Example 1: Scroll Progress Bar

Create a progress bar at the top of the page:

function ScrollProgressBar() {
const { scrollYProgress } = useScroll(window);

return (
<>
<div style={{ height: 3000 }}>
<h1>Scroll down to see progress</h1>
</div>
<animate.div
style={{
position: 'fixed',
top: 0,
left: 0,
width: scrollYProgress.to([0, 1], ['0%', '100%']),
height: 4,
background: 'linear-gradient(90deg, #667eea, #764ba2)',
zIndex: 1000,
}}
/>
</>
);
}

Example 2: Scale on Scroll

Scale an element based on scroll progress:

function ScaleOnScroll() {
const { scrollYProgress } = useScroll(window);

return (
<div style={{ height: 2000 }}>
<animate.div
style={{
position: 'fixed',
top: '50%',
left: '50%',
translateX: -50,
translateY: -50,
scale: scrollYProgress.to([0, 1], [1, 2]),
width: 100,
height: 100,
background: 'teal',
borderRadius: 8,
}}
/>
</div>
);
}

Example 3: Color Interpolation

Change color based on scroll progress:

function ColorOnScroll() {
const { scrollYProgress } = useScroll(window);

return (
<div style={{ height: 2000 }}>
<animate.div
style={{
position: 'fixed',
top: '50%',
left: '50%',
translateX: -50,
translateY: -50,
width: 200,
height: 200,
backgroundColor: scrollYProgress.to([0, 1], ['#0069d9', '#ff5733']),
borderRadius: 8,
}}
/>
</div>
);
}

Example 4: Element-Specific Progress

Track progress relative to a specific element:

function ElementProgress() {
const containerRef = useRef(null);
const { scrollYProgress } = useScroll(window, {
target: containerRef,
offset: ['start end', 'start start'],
});

return (
<div style={{ height: 2000 }}>
<animate.div
ref={containerRef}
style={{
marginTop: 800,
padding: 40,
background: 'teal',
borderRadius: 8,
opacity: scrollYProgress.to([0, 1], [0, 1]),
translateY: scrollYProgress.to([0, 1], [50, 0]),
}}
>
<h2>This element animates as you scroll</h2>
<p>Progress: {Math.round(scrollYProgress.get() * 100)}%</p>
</animate.div>
</div>
);
}

Example 5: Smooth Progress with Spring

Add spring animation to progress value:

function SmoothProgress() {
const { scrollYProgress } = useScroll(window, {
toDescriptor: (p) => withSpring(p, { damping: 10 }),
});

return (
<div style={{ height: 2000 }}>
<animate.div
style={{
position: 'fixed',
top: 0,
left: 0,
width: scrollYProgress.to([0, 1], ['0%', '100%']),
height: 4,
background: 'teal',
}}
/>
</div>
);
}

Example 6: Horizontal Scroll Progress

Track horizontal scroll progress:

function HorizontalProgress() {
const containerRef = useRef(null);
const { scrollXProgress } = useScroll(containerRef);

return (
<div
ref={containerRef}
style={{
width: 400,
height: 300,
overflowX: 'auto',
border: '1px solid #e1e1e1',
}}
>
<div style={{ width: 2000, height: 200, padding: 20 }}>
<animate.div
style={{
position: 'sticky',
top: 0,
left: 0,
width: scrollXProgress.to([0, 1], ['0%', '100%']),
height: 4,
background: 'teal',
}}
/>
<p>Scroll horizontally to see progress</p>
</div>
</div>
);
}

Offset Configuration

Control when progress starts and ends using offset strings:

const { scrollYProgress } = useScroll(window, {
target: elementRef,
offset: ['start end', 'start start'],
});

Offset Values

  • 'start' - Start of the element/viewport
  • 'end' - End of the element/viewport
  • 'center' - Center of the element/viewport

Common Patterns

// Progress from when element enters viewport to when it leaves
offset: ['start end', 'end start'];

// Progress from when element enters viewport to when it reaches top
offset: ['start end', 'start start'];

// Progress from when element is 50% visible to when it's fully visible
offset: ['start center', 'start start'];

Interpolation Patterns

Use the to function to map progress (0-1) to any range:

Pattern 1: Linear Mapping

// Map 0-1 to 0-100
width: scrollYProgress.to([0, 1], [0, 100]);

// Map 0-1 to 100-0 (reverse)
opacity: scrollYProgress.to([0, 1], [1, 0]);

Pattern 2: Multi-Stage Animation

// Different values at different progress points
scale: scrollYProgress.to([0, 0.5, 1], [1, 1.5, 1]);

Pattern 3: Color Transitions

backgroundColor: scrollYProgress.to(
[0, 0.5, 1],
['#0069d9', '#ff5733', '#00d9ff']
);

Common Patterns

Pattern 1: Progress Indicator

const { scrollYProgress } = useScroll(window);

<animate.div
style={{
width: scrollYProgress.to([0, 1], ['0%', '100%']),
}}
/>;

Pattern 2: Fade In/Out

const { scrollYProgress } = useScroll(window, {
target: elementRef,
offset: ['start end', 'start start'],
});

<animate.div
style={{
opacity: scrollYProgress.to([0, 1], [0, 1]),
}}
/>;

Pattern 3: Parallax Effect

const { scrollYProgress } = useScroll(window);

<animate.div
style={{
translateY: scrollYProgress.to([0, 1], [0, -200]),
}}
/>;

Best Practices

✅ Do

  • Use scrollYProgress for vertical scroll effects
  • Use scrollXProgress for horizontal scroll effects
  • Configure offsets to control when progress starts/ends
  • Use the to function for smooth value interpolation
  • Combine with withSpring for smoother progress updates

❌ Don't

  • Don't use progress values directly in calculations (use to function)
  • Don't forget to handle horizontal vs vertical progress
  • Don't use progress for elements that don't scroll
  • Don't make progress calculations too complex

Performance Tips

  1. Use transforms - translateX, translateY, scale are GPU-accelerated
  2. Limit calculations - Progress is calculated automatically, avoid manual calculations
  3. Use offsets wisely - Configure offsets to only track relevant scroll ranges
  4. Smooth with spring - Use toDescriptor with withSpring for smoother updates

Troubleshooting

Progress doesn't update:

  • Make sure the element is scrollable
  • Check that the ref is attached correctly
  • Verify the element has scrollable content

Progress jumps:

  • Use toDescriptor with withSpring for smoother updates
  • Check that offsets are configured correctly
  • Ensure the target element exists

Progress is always 0 or 1:

  • Verify the scroll container has enough content to scroll
  • Check offset configuration
  • Make sure you're tracking the correct scroll direction

Next Steps