Skip to main content
Version: Next

useScroll

The useScroll hook captures scroll activity on any DOM element or the window. It provides detailed scroll information including position, velocity, and movement deltas - perfect for scroll-triggered animations, parallax effects, and progress indicators.

Why Use useScroll?

useScroll simplifies scroll interactions by:

  • Tracking scroll position - Know exactly where users have scrolled
  • Providing velocity - Detect scroll speed for momentum effects
  • Calculating deltas - Track scroll changes between frames
  • Multiple targets - Monitor scroll on multiple elements
  • Cross-browser support - Consistent behavior across browsers

Basic Syntax

useScroll(
refs: RefObject<HTMLElement> | RefObject<HTMLElement>[] | Window,
callback: (event: ScrollEvent & { index: number }) => void
): void

Parameters

  • refs (required): Element(s) or window to monitor for scroll
  • callback (required): Function called on scroll with event data

Simple Example

Track window scroll position:

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

function ScrollTracker() {
const [scrollY, setScrollY] = useValue(0);

useScroll(window, ({ offset }) => {
setScrollY(offset.y);
});

return (
<div style={{ height: 2000 }}>
<div style={{ position: 'fixed', top: 20, left: 20 }}>
Scroll Y: {Math.round(scrollY)}
</div>
</div>
);
}
import React, { useState } from 'react';
import { useScroll } from 'react-ui-animate';

const App = () => {
  const [scrollPosition, setScrollPosition] = useState(0);

  useScroll(window, function (event) {
    setScrollPosition(event.offset.y);
  });

  return (
    <div style={{ height: 2000 }}>
      <div style={{ position: 'fixed', left: 10, top: 10 }}>
        SCROLL POSITION: {scrollPosition}
      </div>
    </div>
  );
};

export default App;

Scroll Event Data

The callback receives a ScrollEvent object:

type ScrollEvent = {
index: number; // Index of element (for arrays)
movement: { x: number; y: number }; // Change since last frame
offset: { x: number; y: number }; // Current scroll position
velocity: { x: number; y: number }; // Scroll velocity in px/ms
event: Event; // Native scroll event
cancel?: () => void; // Cancel gesture (optional)
};

Real-World Examples

Example 1: Scroll Progress Bar

Show scroll progress at the top of the page:

function ScrollProgress() {
const [progress, setProgress] = useValue(0);

useScroll(window, ({ offset }) => {
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
const progressValue = Math.min(1, Math.max(0, offset.y / maxScroll));
setProgress(progressValue);
});

return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
width: `${progress * 100}%`,
height: 4,
background: 'teal',
zIndex: 1000,
}}
/>
);
}

Example 2: Parallax Effect

Create a parallax scrolling effect:

function ParallaxSection() {
const [parallax, setParallax] = useValue(0);

useScroll(window, ({ offset }) => {
// Parallax moves slower than scroll
setParallax(offset.y * 0.5);
});

return (
<div style={{ height: '200vh' }}>
<animate.div
style={{
position: 'fixed',
top: 0,
translateY: parallax,
width: '100%',
height: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
}}
>
<h1>Parallax Content</h1>
</animate.div>
</div>
);
}

Example 3: Fade on Scroll

Fade element based on scroll position:

function FadeOnScroll() {
const [opacity, setOpacity] = useValue(0);

useScroll(window, ({ offset }) => {
// Fade in as user scrolls
const fadeStart = 200;
const fadeEnd = 600;
const opacityValue = Math.max(0, Math.min(1,
(offset.y - fadeStart) / (fadeEnd - fadeStart)
));
setOpacity(opacityValue);
});

return (
<div style={{ height: 2000 }}>
<animate.div
style={{
position: 'fixed',
top: '50%',
left: '50%',
translateX: -50,
translateY: -50,
opacity,
padding: 40,
background: 'white',
borderRadius: 8,
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
}}
>
<h2>Fade on Scroll</h2>
<p>Scroll to see me fade in</p>
</animate.div>
</div>
);
}

Example 4: Scroll-Triggered Animation

Animate element when it enters viewport:

function ScrollReveal() {
const [revealed, setRevealed] = useState(false);
const [translateY, setTranslateY] = useValue(100);
const [opacity, setOpacity] = useValue(0);
const ref = useRef(null);

useScroll(window, ({ offset }) => {
if (!ref.current) return;

const rect = ref.current.getBoundingClientRect();
const viewportTop = window.innerHeight;

// Check if element is in viewport
if (rect.top < viewportTop && rect.bottom > 0) {
if (!revealed) {
setRevealed(true);
setTranslateY(withSpring(0));
setOpacity(withSpring(1));
}
}
});

return (
<div style={{ height: 2000 }}>
<animate.div
ref={ref}
style={{
translateY,
opacity,
padding: 40,
background: 'teal',
borderRadius: 8,
marginTop: 800,
}}
>
Scroll to reveal
</animate.div>
</div>
);
}

Example 5: Scroll Container

Track scroll within a specific container:

function ScrollableContainer() {
const ref = useRef(null);
const [scrollX, setScrollX] = useValue(0);
const [scrollY, setScrollY] = useValue(0);

useScroll(ref, ({ offset }) => {
setScrollX(offset.x);
setScrollY(offset.y);
});

return (
<div
ref={ref}
style={{
width: 400,
height: 300,
overflow: 'auto',
border: '1px solid #e1e1e1',
borderRadius: 8,
}}
>
<div style={{ width: 800, height: 600, padding: 20 }}>
<p>Scroll X: {Math.round(scrollX)}</p>
<p>Scroll Y: {Math.round(scrollY)}</p>
<p>Scroll this container to see the values update</p>
</div>
</div>
);
}

Common Patterns

Pattern 1: Scroll Progress

const [progress, setProgress] = useValue(0);

useScroll(window, ({ offset }) => {
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
setProgress(offset.y / maxScroll);
});

Pattern 2: Sticky Header

const [headerOpacity, setHeaderOpacity] = useValue(0);

useScroll(window, ({ offset }) => {
// Fade in header after scrolling 100px
const opacity = Math.min(1, offset.y / 100);
setHeaderOpacity(opacity);
});

Pattern 3: Scroll Velocity Effects

const [scale, setScale] = useValue(1);

useScroll(window, ({ velocity }) => {
// Scale based on scroll velocity
const velocityMagnitude = Math.abs(velocity.y);
const newScale = 1 + velocityMagnitude * 0.01;
setScale(withSpring(Math.min(1.2, newScale)));
});

Best Practices

✅ Do

  • Use useScroll for scroll-triggered animations
  • Combine with useValue for smooth scroll-based animations
  • Calculate progress as a 0-1 value for easier interpolation
  • Use offset for absolute scroll position
  • Use movement for scroll deltas

❌ Don't

  • Don't do expensive operations in scroll callbacks (debounce if needed)
  • Don't forget to handle edge cases (very fast scrolling, etc.)
  • Don't track scroll on too many elements simultaneously
  • Don't use layout properties - use transforms for scroll-based animations

Performance Tips

  1. Debounce expensive operations - Scroll events fire frequently
  2. Use transforms - translateY, translateX are GPU-accelerated
  3. Throttle calculations - Don't recalculate on every scroll event
  4. Use requestAnimationFrame - For smooth animations tied to scroll

Troubleshooting

Scroll doesn't trigger:

  • Make sure the element has overflow: auto or overflow: scroll
  • Check that the element is actually scrollable
  • Verify the ref is attached correctly

Performance issues:

  • Debounce or throttle expensive operations
  • Use transforms instead of layout properties
  • Limit the number of scroll listeners

Next Steps