The Build Series: E5 - Page Scroll Component in React
- Published on
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
:
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) // highlight-line
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 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’s 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 scrollPosition
}
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:
And there we have it! We've built a scroll indicator by leveraging the scroll
events and some React hooks magic.
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! ✌🏽
On this page