The Build Series: E5 - Page Scroll Component in ReactMay 14, 2021The Build Series 423 words
The Build Series: E5 - Page Scroll Component in React
Photo by Kelly Sikkema on Unsplash

Intro

You might have seen websites with a progress bar at the top of the page which shows how much % of page a user has scrolled. Real fancy stuff. In this episode of The Build Series, we’ll build a progress bar which shows the current scroll position of the page.

ProgressBar Indicator Component

First let’s build a simple progress bar.

import React from 'react';
import from './ProgressIndicator.scss';
export const ProgressIndicator = ({ progress }) => {
	return (
		<div className='progress-wrapper'>
			<div className='progress-track' style={{ width: `${progress}%` }}></div>
		</div>
	);
};
.progress-wrapper {
		border-radius: 8px;
		border: 2px solid #444;
		box-sizing: border-box;
		height: 16px;
		margin-bottom: 8px;
		width: 100%;

		.progress-track {
			background-color: #61afef;
			border: 2px solid #222;
			border-radius: 9px;
			height: 8px;
			padding: 0;
			animation: progressBar 1.5s ease-in-out;
			animation-fill-mode: both;
		}
	}

The ProgressBar component will look something like this, when progress = 40:

image-20210515005059625

Although the semantically correct way to build a progress bar is using <Progress> HTML tag, for the sake of ease of styling, we’ll use <div> this time around.

Listen to Page Scroll

Next we need to listen to the page scroll event. We will use the scroll event and update the progress prop of the ProgressBar component. Since scroll events fire at a very high rate, we will also throttle the event using requestAnimationFrame().

Let’s add a scroll listener, and because we are using React, we will wrap it within a useEffect.

useEffect(() => {
		document.addEventListener('scroll', listener);		return () => {
			document.removeEventListener('scroll', listener);
		};
	});

And our listener will call requestAnimationFrame and calculate the scroll distance.

const listener = () => {
		requestAnimationFrame(() => {
			calculateScrollDistance();
		});
	};

Pretty straightforward all this. We now have a scroll event listener which fires an event whenever user scrolls the page. We also use requestAnimationFrame() method to tell the browser that we wish to perform an operation and requests that the browser calls a specified function to update before the next repaint.

Calculating Scroll Distance

Now this is a part where we gotta use some math. To calculate how much % of a page user has scrolled, we will need reference to two things:

  • The height of the document
  • The current scroll location

Getting the height of the document is a tricky one. Different browsers have different methods of reporting scrollHeight. Although, Can I Use states scrollHeight is supported by all browsers, we will get all height values found on document and use the highest one for compatibility sake.

const getDocHeight = () => {
		return Math.max(
			document.body.scrollHeight,			document.documentElement.scrollHeight,			document.body.offsetHeight,			document.documentElement.offsetHeight,			document.body.clientHeight,			document.documentElement.clientHeight		);
	};

And to calculate scroll distance:

const calculateScrollDistance = () => {
		const scrollTop = window.pageYOffset; // how much the user has scrolled by
		const winHeight = window.innerHeight;
		const docHeight = getDocHeight();

		const totalDocScrollLength = docHeight - winHeight;
		const scrollPosition = Math.floor((scrollTop / totalDocScrollLength) * 100);
		return scrollPostion
	};

Putting everything together in a hook,

export const usePageScroll = (handler) => {
	const calculateScrollDistance = () => {
		const scrollTop = window.pageYOffset; // how much the user has scrolled by
		const winHeight = window.innerHeight;
		const docHeight = getDocHeight();

		const totalDocScrollLength = docHeight - winHeight;
		const scrollPosition = Math.floor((scrollTop / totalDocScrollLength) * 100);

		handler(scrollPosition);	};

	const getDocHeight = () => {
		return Math.max(
			document.body.scrollHeight,
			document.documentElement.scrollHeight,
			document.body.offsetHeight,
			document.documentElement.offsetHeight,
			document.body.clientHeight,
			document.documentElement.clientHeight
		);
	};

	const listener = () => {
		requestAnimationFrame(() => {
			calculateScrollDistance();
		});
	};
	useEffect(() => {
		document.addEventListener('scroll', listener);
		return () => {
			document.removeEventListener('scroll', listener);
		};
	});
};

Using the hook

Let’s use the usePageScroll hook. (See what I did there 😏). We maintain a progress state, which gets updated whenever user scrolls the page. This is passed as props to ProgressIndicator component, which updates the progress bar.

const repeater = Array.from({ length: 10 }, (i) => "i");

const Page = () => {
	const [progress, setProgress] = useState(0); // Initial progress = 0;
	usePageScroll(setProgress);	
	return(
		<div className="App">
      <div className="navigation-bar">
        <ProgressIndicator progress={progress} />       </div>
      {repeater.map((_, i) => (
        <p key={i}>
          Aute officia nulla deserunt do deserunt cillum velit magna. Officia
          veniam culpa anim minim dolore labore pariatur voluptate id ad est
          duis quis velit dolor pariatur enim. Incididunt enim excepteur do
          veniam consequat culpa do voluptate dolor fugiat ad adipisicing sit.
          Labore officia est adipisicing dolore proident eiusmod exercitation
          deserunt ullamco anim do occaecat velit. Elit dolor consectetur
          proident sunt aliquip est do tempor quis aliqua culpa aute. Duis in
          tempor exercitation pariatur et adipisicing mollit irure tempor ut
          enim esse commodo laboris proident. Do excepteur laborum anim esse
          aliquip eu sit id Lorem incididunt elit irure ea nulla dolor et. Nulla
          amet fugiat qui minim deserunt enim eu cupidatat aute officia do velit
          ea reprehenderit. Voluptate cupidatat cillum elit quis ipsum eu
          voluptate fugiat consectetur enim. Quis ut voluptate culpa ex anim
          aute consectetur dolore proident voluptate exercitation eiusmod. Esse
          in do anim magna minim culpa sint. Adipisicing ipsum consectetur
          proident ullamco magna sit amet aliqua aute fugiat laborum
          exercitation duis et.
        </p>
      ))}
    </div>
	)
}

The end result should look something like this:

Demo

And there we have it! We’ve built a scroll indicator by leveraging the scroll events and some React hooks magic.

Edit upbeat-cdn-g3bdb

Bonus

See how the navigation bar is sticking to the top of the page, to achieve that we use a CSS property called position: sticky. The CSS for the navigation-bar class is:

.navigation {
  height: 18px;
  width: 100%;
  background-color: #191a1d;
  position: sticky;  top: 0;  padding: 12px 0;
}

That’s it for this episode of The Build Series. Feel free to reach out if you need any help!

Until next time! ✌🏽

Read next
Spotlight Software: E1 - Battle of the Markdown Editors
Spotlight Software: E1 - Battle of the Markdown EditorsThere are a lot of awesome markdown editors out in the wild. In this episode, I talk about full fleged WYSIWYG editors to simple text editors and crown my favourite editor
The Lazy Developer Series: E2 — Automating workflows using a CLI
The Lazy Developer Series: E2 — Automating workflows using a CLIIn this episode of The Build Series, we will build a CLI to automate creation of new blog posts