Knowledge Base/How to track page scroll and use it for personalization

How to track page scroll and use it for personalization

how-toDeveloperPersonalizationAnalyticsNextJS app router

Scroll-based personalization lets you match content to visitor engagement levels. Users who scroll deeper show more interest, making them ideal candidates for stronger calls-to-action or related offers, while those who scroll less might need simpler engagement options. This approach improves conversions by delivering the right message at the right moment based on demonstrated user interest.

General idea

Track when users reach specific scroll milestones and use that data to personalize content in Uniform Canvas. In the example below, we use 25%, 50%, 75%, 100%, but it is possible to define any % using the same approach.

This example is for latest NextJS App Router Uniform SDK. If you need to have this functionality working on any other framework or earlier SDK versions, please reach out to us via support portal at https://support.uniform.dev/

Implementation

You can find these steps implemented here in our example repo.

Step 1: Create a Scroll Tracker Component

Create a component that tracks scroll milestones and updates Uniform context

This is a sample code which may need to be optimized for better performance
'use client'; import { useEffect } from 'react'; import { useUniformContext } from '@uniformdev/canvas-next-rsc/component'; export default function ScrollTracker() { const { context } = useUniformContext(); useEffect(() => { const handleScrollMilestone = async (percentage: number) => { if (percentage && context) { context.update({ quirks: { scrollPosition: `${percentage}p`, }, }); } document.dispatchEvent( new CustomEvent('scroll_milestone', { detail: { percentage, milestone: `${percentage}%`, }, }) ); }; const handleScroll = () => { const scrollTop = window.scrollY || document.documentElement.scrollTop; const scrollHeight = document.documentElement.scrollHeight - window.innerHeight; const scrollPercentage = Math.round((scrollTop / scrollHeight) * 100); const milestones = [25, 50, 75, 100]; milestones.forEach(milestone => { if (scrollPercentage === milestone) { handleScrollMilestone(milestone); } }); }; window.addEventListener('scroll', handleScroll, { passive: true }); handleScroll(); return () => { window.removeEventListener('scroll', handleScroll); }; }, [context]); // Remove context from dependencies to prevent re-runs return null; }

You can also reset the quirk value to 0p on the page load, so every time a visitor opens, it track scroll position from 0. To do that, you can add this code to be executed server side, in the file where the ContextUpdateTransfer is defined:

<ContextUpdateTransfer serverContext={serverContext} update={{ quirks: { scrollPosition: '0p', }, }} />

Step 2: Add to Your Layout

Include the ScrollTracker in your app layout so it will be loaded for any page

Make sure this is added inside the <UniformContext> component, otherwise updating quirks and personalization won't work
import ScrollTracker from '@/components/scroll-tracker'; export default function RootLayout({ children }) { return ( <html> <body> <UniformContext> {children} <ScrollTracker /> </UniformContext> </body> </html> ); }

Step 3: Create Quirks

We will create milestones as quirks, which can than be used for personalization rules. Here is a sample quick set up, which corresponds to the code above:

image.png

Step 4: Personalize

To be able to use quirks in personalization rules without signals, you need to run Uniform SDK 20+. If you need to
  1. Add personalization component in the structure
  2. Set personalization rules based on quirks, similar to this:
image.png

Step 5: Track scroll into analytics (optional)

You may want to track how much page a visitor saw and review this data in the analytics reports. If you enabled Uniform tracker to push data into your analytics (for example, by following the steps for GA4 plugin), you should see all events when personalization based on the scroll was triggered on a page. If you want to track all page scrolls, no matter if the personalization happened or not, here are sample steps:

  1. Make sure the gtag script is added and working
  2. Update the scroll tracker code like this:

scroll-tracker.tsx

export default function ScrollTracker() { const { context } = useUniformContext(); useEffect(() => { const handleScrollMilestone = async (percentage: number) => { if (percentage && context) { context.update({ quirks: { scrollPosition: `${percentage}p`, }, }); } gtag('event', 'scroll_depth', { scroll_percent: percentage, page_path: window.location.pathname, page_title: document.title, }); document.dispatchEvent( new CustomEvent('scroll_milestone', { detail: { percentage, milestone: `${percentage}%`, }, }) ); }; const handleScroll = () => { const scrollTop = window.scrollY || document.documentElement.scrollTop; const scrollHeight = document.documentElement.scrollHeight - window.innerHeight; const scrollPercentage = Math.round((scrollTop / scrollHeight) * 100); const milestones = [25, 50, 75, 100]; milestones.forEach(milestone => { if (scrollPercentage === milestone) { handleScrollMilestone(milestone); } }); }; window.addEventListener('scroll', handleScroll, { passive: true }); handleScroll(); return () => { window.removeEventListener('scroll', handleScroll); }; }, [context]); // Remove context from dependencies to prevent re-runs return null; }
Published: July 11, 2025