isPublished: true title: Tracking Scroll Progress and Direction with Motion for React description: Do you need to track scroll progress and direction for your scroll-linked animations? We'll look at how to easily accomplish this using Motion for React. date: '2024-12-16' categories:

  • react
  • motion for react
  • framer motion

Tracking Scroll Progress and Direction with Motion for React

0%
Scroll direction is down
Scroll progress: 0%

The building blocks of scroll-driven animations are pretty simple to set up using Motion for React.

We can track page scroll and scroll direction in just a few lines of code.

Scroll Progress

const { scrollYProgress } = useScroll()

scrollYProgress is an automatically tracked motion value between 0 and 1. It approaches 1 as we scroll toward the bottom of the page.

Scroll Direction

To track scroll direction, we just need to take the difference between the current and previous scrollYProgress.

We'll use React state to keep track of the current scroll direction and use Motion's useMotionValueEvent to update our state any time scrollYProgress changes.

We'll pass in the current motion value as an argument to our handler. Then we'll get the previous value of scrollYProgress by calling getPrevious() and simply subtract it from the current value.

The resulting diff tells us the current scroll direction.

We're using 1 and -1 here to represent scrolling down and up, respectively.

One nuance here is that the change event fires on every scroll frame, potentially 60+ times per second. If we call setScrollDirection on every frame, we trigger a React re-render each time, even when the direction hasn't changed. That adds up fast, especially if multiple components consume this hook.

To avoid this, we use a ref to track the current direction and only call setScrollDirection when it actually changes. During a typical scroll gesture, the direction stays constant across dozens of frames, so this short-circuits the vast majority of unnecessary state updates.

const [scrollDirection, setScrollDirection] = useState(1)
// Track direction outside of React state so we can compare
// without triggering re-renders on every scroll frame.
const directionRef = useRef(1)
 
useMotionValueEvent(scrollYProgress, 'change', (current) => {
  const prev = scrollYProgress.getPrevious()
  if (!prev) return
  const newDirection = current - prev > 0 ? 1 : -1
  // Only update state when direction actually changes,
  // avoiding unnecessary re-renders during continuous scrolling.
  if (directionRef.current !== newDirection) {
    directionRef.current = newDirection
    setScrollDirection(newDirection)
  }
})

Compose a Hook

We can also build a hook to simplify getting scroll progress and direction in any component that needs it.

// hooks/useScrollDirection.ts
export default function useScrollDirection() {
  const { scrollYProgress } = useScroll()
  const [scrollDirection, setScrollDirection] = useState(1)
  // Track direction outside of React state so we can compare
  // without triggering re-renders on every scroll frame.
  const directionRef = useRef(1)
 
  useMotionValueEvent(scrollYProgress, 'change', (current) => {
    const prev = scrollYProgress.getPrevious()
    if (!prev) return
    const newDirection = current - prev > 0 ? 1 : -1
    // Only update state when direction actually changes,
    // avoiding unnecessary re-renders during continuous scrolling.
    if (directionRef.current !== newDirection) {
      directionRef.current = newDirection
      setScrollDirection(newDirection)
    }
  })
 
  return { scrollDirection, scrollYProgress }
}

Now we can access our scrollDirection state as well as the scrollYProgress motion value from any component in just one line.

// Inside any client component...
const { scrollDirection, scrollYProgress } = useScrollDirection()

Okay, we're finished

Happy Animating

The interactive component showing in this post is a quick demo to illustrate building some UI based on the code above.

Motion also provides some great examples for inspiration.

Thanks for reading!

Austin Gregersen  ©2026