← Back toDrag and drop in React

Build a custom scrollbar

Written byPhuoc Nguyen
Created
04 Nov, 2023
Last updated
10 Nov, 2023
Tags
React custom scrollbar, webkit-scrollbar
Custom scrollbars can be a great addition to any website or application for several reasons. First, they can improve the user experience by providing a more visually appealing and intuitive way to navigate content. Second, they can help to maintain consistency with the overall design of the website or application. Finally, custom scrollbars can also provide additional functionality, like jump-to links or visual cues to enhance usability.
By using a custom scrollbar instead of the default one, you can make scrolling more engaging and interactive. Plus, you have full control over its appearance and behavior, ensuring that it matches your website or application's design elements and creates a seamless visual experience for your users.
Many modern websites and applications, like Gucci's website and WhatsApp's desktop application, use custom scrollbars to enhance their user experience. By providing unique design elements and additional functionality, custom scrollbars help to create a more engaging and interactive browsing experience for users.
In this post, we'll learn how to create a custom scrollbar with React.

Customizing the default scrollbar with CSS

Did you know that you can customize the default scrollbar in your web browser? With a few lines of CSS, you can change the scrollbar's color, width, and even its shape!
To do this, we use the `::-webkit-scrollbar` pseudo-element. This element lets us target different parts of the scrollbar, like the track and thumb, and give them custom styles.
In the example code below, we're targeting the `::-webkit-scrollbar` element and making its background color transparent. We're also setting the scrollbar's height and width to 8 pixels.
Then, we're targeting the `::-webkit-scrollbar-thumb` element and giving it rounded edges with a `border-radius` of 0.25rem. We're also setting its background color to a shade of gray.
Finally, we're targeting the `::-webkit-scrollbar-track` element and making its background color transparent.
Here's an example of the CSS styles you can use to customize the default scrollbar:
css
::-webkit-scrollbar {
background: transparent;
height: 8px;
width: 8px;
}
::-webkit-scrollbar-thumb {
border-radius: 0.25rem;
background: rgb(148 163 184);
}
::-webkit-scrollbar-track {
background: transparent;
}
In Firefox, we have two new CSS properties that allow us to customize the scrollbar: `scrollbar-width` and `scrollbar-color`, in addition to the `::-webkit-scrollbar` pseudo-element.
The `scrollbar-width` property lets us specify the width of the scrollbar. By default, the browser displays a scrollbar with a default width. However, we can set it to `thin` to create a more subtle and streamlined look for our custom scrollbar.
The `scrollbar-color` property lets us specify the color of the thumb and track areas of the scrollbar. We can use any valid CSS color value, including hex codes, RGB values, and named colors.
Here's an example of how it works:
css
body {
/* Make the scrollbar thin */
scrollbar-width: thin;

/* Set the colors of the thumb and track areas */
/* Thumb color: #718096 */
/* Track color: #edf2f7 */
scrollbar-color: #718096 #edf2f7;
}
By customizing the elements or properties using CSS, you can create a unique scrollbar design that matches your website or application's overall look and feel.
But, there are some limitations to using CSS for customizing the scrollbar. Firstly, custom styles only work on webkit-based browsers such as Chrome and Safari. Firefox and other non-webkit browsers do not support the `::-webkit-scrollbar` pseudo-element and therefore cannot be styled using CSS.
Secondly, while CSS can change the appearance of the scrollbar, it does not allow for much interactivity or functionality beyond that. For example, it is not possible to add smooth scrolling or snap-scrolling without using JavaScript. Although you can add jump-to links with CSS, there are limitations to the additional functionality that can be added.

Hiding the default scrollbar

Believe it or not, making your own scrollbar is a lot easier than you might think. The first step is to hide the default scrollbar, then we can create a brand new one that's completely customizable.
Let's say you want to create a scrollbar for a scrollable element. Here's how you can do it:
tsx
<div className="container">...</div>
To allow scrolling within a content area, you need to use at least two CSS properties: `height` (or `max-height`) to set the maximum height of the content, and `overflow` set to `auto`. With these properties set, users can scroll through the content as needed.
css
.container {
max-height: 24rem;
overflow: auto;
}
To hide the default scrollbar, we can wrap the entire content inside another element:
tsx
<div className="container">
<div className="container__content">
...
</div>
</div>
To make the new container fit the full height, we simply set the `height` property to 100%. Then, we block scrolling in the main element by using `overflow: hidden`. This hides the default scrollbar and prevents users from scrolling through it. Instead, we allow users to scroll through the content by setting `overflow: auto` on the new container. This ensures that only the content area is scrollable, while the rest of the page remains fixed in place. With these properties set, users can easily navigate through your content using your custom scrollbar without any issues.
css
.container {
overflow: hidden;
}
.container__content {
height: 100%;
overflow: auto;
}
To hide the default scrollbar, we can use a simple trick. First, we give our container a negative margin of `-1rem`, which shifts the content area to the left and out of sight. Then, we add `padding-right: 1rem` to the same container, which creates a 1 rem padding on the right side of the content area. This ensures that our content doesn't get chopped off by the negative margin, while still keeping the default scrollbar hidden from view.
css
.container__content {
margin-right: -1rem;
padding-right: 1rem;
}

Building your own scrollbar

Now, let's dive into creating a custom scrollbar. To get started, we'll create two elements: a track and a thumb, contained within the scrollbar. These elements will be appended to the original element as follows:
tsx
<div className="container">
<div className="container__content">...</div>

<div className="scrollbar">
<div className="scrollbar__track" />
<div className="scrollbar__thumb" />
</div>
</div>
To position the scrollbar, we use absolute positioning and set its `top` and `right` properties to 0. This ensures that the scrollbar is positioned at the right edge of the container element. We also set its height to 100% so that it fills the entire height of the container.
The fake scrollbar is made up of two parts: a track and a thumb. The track is a simple `div` element with a background color that represents the full length of the content being scrolled. The thumb is another `div` element that represents the visible portion of the content being scrolled. Its position is updated based on how much of the content is currently visible, allowing users to see their current position within the content.
Here are the basic styles applied to the scrollbar elements:
css
.scrollbar {
position: absolute;
top: 0;
right: 0;

height: 100%;
width: 0.75rem;
}
.scrollbar__thumb {
left: 0;
position: absolute;
width: 100%;
}
.scrollbar__track {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
To customize the background color of scrollbar and thumb elements, we can use CSS' `background-color` property. For instance, if we desire to set the background color of the scrollbar to light gray and the background color of the thumb to darker gray, we can use the following CSS code:
css
.scrollbar {
background-color: rgb(249 250 251);
}
.scrollbar__thumb {
background-color: rgb(156 163 175);
}
We can make our custom scrollbar look exactly the way we want it to by changing these properties.

Setting the initial height of the thumb element

To start off, we need to calculate the height of our scrollbar thumb based on the height of our content. This is done by finding the ratio between the visible portion of the content (`clientHeight`) and the total height of all content (`scrollHeight`). By dividing these two values, we can get a ratio that we can use to set the initial height of our custom scrollbar thumb.
To make sure that the thumb is proportional to the amount of content being displayed on the screen, we set the height of the thumb to the calculated ratio multiplied by 100%. This ensures that the thumb's height is always relative to the amount of content that is visible.
To perform this calculation only once when the component mounts, we can use React's useEffect hook. We use the `useRef` hook to store references to both our thumb and content elements, and then use these references to calculate and set the initial height of our scrollbar thumb.
Here's how you can do it:
ts
React.useEffect(() => {
const thumbEle = thumbRef.current;
const contentEle = contentRef.current;

const scrollRatio = contentEle.clientHeight / contentEle.scrollHeight;
thumbEle.style.height = `${scrollRatio * 100}%`;
}, []);

Making the thumb element draggable

Let's take a moment to revisit the post where we learned how to create a draggable element. In that post, we used the `useRef()` hook to create a reference to the target element. Then, we attached the reference to the element using the `ref` attribute. We also handled the `mousedown` and `touchstart` events to make the target element draggable on both desktop and touchscreen devices.
Now, we're going to use the same approach to make the thumb element draggable.
In case you need a refresher, here's a quick code snippet to remind you of what we did:
tsx
const thumbRef = React.useRef();

// Render
<div
className="scrollbar__thumb"
ref={thumbRef}
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
/>
In the past, we positioned draggable elements by setting their position absolutely within their container and updating their position using the `transform` property. However, we've taken a different approach for our scrollbar. We no longer use internal states like `dx` and `dy`. Instead, we now store the starting point and the current scroll position when users begin to drag the thumb element.
Here's how we handle the `mousedown` event:
ts
const handleMouseDown = (e) => {
const ele = thumbRef.current;
const contentEle = contentRef.current;

const startPos = {
top: contentEle.scrollTop,
x: e.clientX,
y: e.clientY,
};
// ...
};
We begin with three properties: `top`, which shows the scroll position, and `x` and `y`, which indicate the mouse position. If we wish to design a personalized horizontal scrollbar, we should keep track of the `left` property.
ts
const startPos = {
left: contentEle.scrollLeft,
};
Next, as users move their mouse, we need to calculate the vertical distance it moves. Rather than updating internal states like we did before, we can simply update the scroll position of the content element.
ts
const handleMouseMove = (e) => {
const dx = e.clientX - startPos.x;
const dy = e.clientY - startPos.y;
const scrollRatio = contentEle.clientHeight / contentEle.scrollHeight;
contentEle.scrollTop = startPos.top + dy / scrollRatio;
updateCursor(ele);
};
The `handleMouseMove` function does some fancy math to figure out how far the mouse has moved since it was clicked down, both up and down and side to side. It also calculates something called a `scrollRatio`, which is just a fancy way of saying how tall the container is compared to how much you can actually scroll inside it.
Using these numbers, the function updates the `scrollTop` property of the container element by adding a little bit of the distance you moved the mouse to where it started. The amount it adds depends on how much you can actually scroll, so it always scrolls at the same speed no matter how tall or short the container is.
Finally, it calls the `updateCursor` function and passes our thumb element as an argument. This function changes the cursor to `grabbing` and disables text selection using CSS. This gives users a visual cue that they are currently dragging an element.
ts
const updateCursor = (ele) => {
ele.style.cursor = "grabbing";
ele.style.userSelect = "none";
document.body.style.cursor = "grabbing";
document.body.style.userSelect = "none";
};
Similarly, don't forget to reset the cursor when the user lets go of the mouse.
ts
const resetCursor = (ele) => {
ele.style.cursor = "grab";
ele.style.userSelect = "";
document.body.style.cursor = "";
document.body.style.userSelect = "";
};

Updating the thumb position on scroll

When the user both drags the thumb element and scrolls the content element, we need to ensure that the position of the thumb element is correctly updated. To achieve this, we can use the following `scroll` event handler for the content element:
tsx
<div
className="container__content"
ref={contentRef}
onScroll={handleScrollContent}
>
...
</div>
ts
const handleScrollContent = () => {
const thumbEle = thumbRef.current;
const contentEle = contentRef.current;
thumbEle.style.top = `${(contentEle.scrollTop * 100) / contentEle.scrollHeight}%`;
};
The `handleScrollContent()` function updates the position of the thumb element as the user scrolls through the content. Every time the user scrolls, this function is called and calculates the thumb's new position based on how far down the user has scrolled.
To make this happen, we first access the references of both the thumb element and the content element. Then, we calculate a percentage that represents the user's position in the content. We do this by dividing the `scrollTop` property (how many pixels have been scrolled) by the `scrollHeight` property (its total height).
After that, we set this percentage as the new value for our thumb element's `top` CSS property, which positions it within its track. By updating this value every time the user scrolls, we can ensure that our custom scrollbar accurately reflects their current position in the content.

Jumping to a specific point on the track

Did you know that there's another way to scroll through content? Users can jump to a specific point in the content element just by clicking on a particular spot in the track element. It's a quick and easy way to navigate through long pages or documents.
To make this happen, we need to calculate and update the `scrollTop` property for the content element.
tsx
<div
className="scrollbar__track"
ref={trackRef}
onClick={handleClickTrack}
/>
ts
const handleClickTrack = (e) => {
const trackEle = trackRef.current;
const contentEle = contentRef.current;

const bound = trackEle.getBoundingClientRect();
const percentage = (e.clientY - bound.top) / bound.height;
contentEle.scrollTop = percentage * (contentEle.scrollHeight - contentEle.clientHeight);
};
The `handleClickTrack()` function is what we use to handle clicks on our scrollbar track element. When a user clicks on a specific spot in the track, it should take them to the corresponding position in the content element.
First, we get references to both the track and content elements using their respective `useRef()` hooks. Then, we calculate the size and position of the track element using its `getBoundingClientRect()` method.
Next, we calculate a percentage that represents where the user clicked on the track. We do this by subtracting the top position of the bounding rectangle from the Y coordinate of their click event, and then dividing that value by the track's height.
Finally, we update the `scrollTop` property of our content element based on this percentage. We do this by multiplying it by the difference between our content element's full scrollable height and its current viewport height. This gives us an absolute pixel value that corresponds to where in our content we want to jump to.
By handling clicks on our scrollbar's track element in this way, we can provide users with an easy way to navigate long pages or documents without having to manually scroll through them.

Demo

We hope this post is easy to follow and not too long. Now it's time to check out the demo below. Try clicking and dragging the fake scrollbar to see how it works just like the real one.

Conclusion

In conclusion, CSS can be used to create a custom scrollbar, but it has its limitations. It only works on webkit-based browsers and doesn't offer much interactivity or functionality beyond changing the scrollbar's appearance. However, we can create a customizable and interactive scrollbar that works across all browsers by making a fake scrollbar and using JavaScript to handle user interactions.
This post shows how to create a vertical scrollbar, but you can use the same approach to implement a horizontal scrollbar with ease. Additionally, we only attach the scrollbar to a given element, but there's room for improvement if you want to attach the scrollbar to the entire document. We'll leave these tasks for you to tackle.
By using the techniques discussed in this post, you can enhance the user experience of your scrollable content and provide users with an intuitive and user-friendly way to navigate through your content.
If you found this post helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

Questions? 🙋

Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Ask me questions

Recent posts ⚡

Newsletter 🔔

If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn