apocalypse

dev

Creating page scroll indicator in a React

#Creating a page scroll indicator

#What do we have to do

Let's think for a few moments, what exactly we want to achieve and break it down to simple things. We want a progress bar. Progress written as a number basically means a certain fraction of a whole (so the % sounds like the best unit for that). Our whole would be an entire page scrolled down to the bottom. Our 0% would be our page not scrolled at all. Whenever our scrolling position changes, we need to calculate at what percentage of the page our scroll is.

#How to implement progress bar

Our actual progress bar, in terms of implementation, can be displayed as two divs, let's call them #outer and #inner. I'm sure you can achieve this type of behavior using some pseudoelements as well, but don't get distracted. We can think of optimizations later on. So we have the two divs. #outer will always take entire available width (so 100%), while #inner's width will be in range from 0% to 100%. #outer can be transparent (or black), while #inner must have some vibrant background color, to indicate the progress.

#How to measure scroll percentage

Browsers provide us, through Document API, with a few properties, that could help us. I'd argue that there are actually way too many properties and that it's no longer helpful. Let's take a quick look at them:

  • document.body.scrollHeight - represents the entire height of the body of the document, including any content that overflows and is not currently visible in the viewport.
  • document.body.offsetHeight - the total height of the body element, including padding and borders, but excluding margins.
  • document.documentElement.clientHeight - the height of the viewport (the visible part of the document in the browser window) excluding the browser's chrome (like the address bar and toolbars).
  • document.documentElement.scrollHeight - the entire height of the document's root element (usually the <html> element), including any content that overflows and is not currently visible in the viewport.
  • document.documentElement.offsetHeight - total height of the document's root element, including padding and borders but excluding margins.

That's crazy. I know.That's crazy. I know.

We could use Math.max(...aboveValues) to be safe... or just go with the document.documentElement.scrollHeight since it's the most reliable value of them all. We are also gonna need the documentElement.clientHeight, to calculate the total available space for our scroll to move through.

Why? Well, when we are at the top of the page, our scrolling position will be 0. Let's assume our browser viewport is 1000px and our total page height is 4000px. Now, when we will be at the bottom of the page, what do you think will be the value of the scroll position? Since you can't answer, I'll do it - it will be 3000px, because the scroll position value is referencing the top of the scrolled view, not the bottom of it. That's why our starting scroll position is 0.

So to get the total available space we will need to subtract scrollHeight - clientHeight.

#Getting our current scrolling position

But how to get the scrolling position in the first place? This would be much more straightforward. We can use the document.documentElement.scrollTop property.

#Let's write some code

#React component boilerplate

We can begin from creating boilerplate of our component. As mentioned before, I'm gonna use two divs, one inside the other. I added the "use client" directive, because I'm gonna use this component in the root component of the page, which is a server component.

ScrollIndicator.tsx
'use client'; // if you're using the app router with RSC function ScrollIndicator() { return ( <div className='fixed top-0 left-0 z-20 bg-black w-full'> <div className='h-2 bg-primary-500 w-0' /> </div> ); } export default ScrollIndicator;

I already added some basic styling in tailwind, but if you would rather stick to the old school CSS, here's the corresponding CSS code:

scroll-indicator.css
#outer { position: fixed; top: 0; left: 0; z-index: 20; /* you can adjust it to your existing CSS elements placement */ background: black; width: 100%; } #inner { height: 0.5rem; background: red; /* use whatever fits your website */ width: 0; }

#Adding scroll event listener

The next step will be to add an event listener for scroll event of our window. In react, we can do it using useEffect, like this:

function handleScrollEvent() { console.log("I'm scrolling the page lol"); } useEffect(() => { window.addEventListener('scroll', handleScrollEvent); return () => { // that part is important, to avoid memory leaks window.removeEventListener('scroll', handleScrollEvent); }; }, []);

#Writing actual logic for determining scroll progress

Now it's time for some actual code. This would still be fairly simple.

ScrollIndicator.tsx
function ScrollIndicator() { const innerRef = useRef<HTMLDivElement>(null); function handleScrollEvent() { if (!innerRef.current) { return; } const { scrollTop: currentScrollPosition, scrollHeight: totalPageHeight, clientHeight: viewportHeight, } = document.documentElement; const availableScrollSpace = totalPageHeight - viewportHeight; const scrollPercentage = (currentScrollPosition / availableScrollSpace) * 100; innerRef.current.style.width = `${scrollPercentage}%`; } ... return ( <div className='...'> <div className='...' ref={innerRef} /> </div> ); }

Let's break this down.

I used useRef to be able to dynamically assign style rules to my inner div. We could use useState instead, and keep the percentage value there, but since we're gonna need this percentage only to set inner element's styles, there is no added benefit of keeping this value in state.

Since all the values that we need for calculations are coming from the document.documentElement object, we can destructure them and assign them some more explanatory names. That's just syntactic sugar for

const currentScrollPosition = document.documentElement.scrollTop; const totalPageHeight = document.documentElement.scrollHeight; const viewportHeight = document.documentElement.clientHeight;

What's really important are these three lines:

const availableScrollSpace = totalPageHeight - viewportHeight; const scrollPercentage = (currentScrollPosition / availableScrollSpace) * 100; innerRef.current.style.width = `${scrollPercentage}%`;

We calculate the percentage, as described a few sections before, and then assign the value to the inner element's width CSS rule.

And that's it. This is how it looks like in action:

Final effectFinal effect

Related tags:

scrollbar
progress
indicator
without
library
scroll
bar
react