Your design system might require a specific visual aesthetic for the scrollbars.
Maybe you want them to disappear when the container is not hovered, or maybe you need custom colors, shapes, and hover effects. Unfortunately, the native ability to style scrollbars is very limited, so if you face any of those requirements, your only option is to create a custom component. To do so, you would need to handle the following tasks:
- You need to know when to show scrollbars
- You need to know how large a bar should be
- You need to know where to position the bar
- You need to be able to drag a bar to scroll the container
- The bar needs to respond to scrolling that happens in other ways
We can implement that 4th one with a somewhat trivial drag-and-drop interaction. And for the rest, you would typically use a combination of ResizeObserver and a scroll event listener to do some JavaScript arithmetic and compute all the values you need. However, that might cause reflows when reading offsetHeight/scrollHeight; scroll events fire multiple times per second, and manually updating DOM values is a costly operation required to keep the scrollbar accurately positioned and sized.
But what if I told you that in 2026 you no longer need JavaScript to do those at all? Let’s explore how scroll-driven animations (available on Chrome, Safari and Firefox Nightly) can do all the work for us.
Accessibility Disclaimer
I’m not an accessibility expert, and as with all things custom, there might be some caveats I’ve missed. But since this technique doesn’t interfere with native scrolling mechanics and focuses solely on bar customization, it should be fine for most users.
Placing The Scrollbars
Before we get to the task at hand, we need to figure out how to position the bars inside our scroll container. We want them fixed to the container frame, but if we use absolute positioning, they will scroll out of view with the rest of the content. Previously, I wrote about this situation and how position: sticky, with clever margin/floating hacks, can help us achieve what we are looking for. You should definitely check that article out if you like styling puzzles to scratch your head about. But here we are focusing on cutting-edge CSS of 2026, and therefore we can rely on a modern solution – anchor positioning. Here’s the general outlook of how our component would look:
<scrollbar>
…
<bars>
<bar />
<bar />
</bars>
</scrollbar>Code language: HTML, XML (xml)
While the scrollbar obviously has two axes, we will only focus on the vertical one for simplicity. Everything I explain here is equally applicable to the horizontal axis. To place our bars, we will define an anchor on the scrollbar and use it in the bars element:
.scrollbar {
anchor-scope: --scrollbar;
anchor-name: --scrollbar;
}
.bars {
pointer-events: none;
position: absolute;
position-anchor: --scrollbar;
inset: anchor(top) anchor(right) anchor(bottom) anchor(left);
}Code language: CSS (css)
Now our bars container is perfectly positioned on top of the scroll container regardless of scrolling, and we can use absolute positioning inside it to place the actual bars. We also add an anchor-scope rule — we do not want the bars to snap to any other scrollbar on the page.
Setting Up Animations
Our animations would rely on the scroll progress. First, we will hide native scrollbars, define timeline-scope and scroll-timeline (default axis is vertical) as well as default variable values:
.scrollbar {
scrollbar-width: none;
scroll-timeline: --scrollbar;
timeline-scope: --scrollbar;
--opacity: 0;
--events: none;
}Code language: CSS (css)
To set the bar’s position using variables, we first need to define them as numbers so they can be animated. And then set basic animations to track progress:
@property --start {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --end {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@keyframes top {
from { --start: 1 }
to { --start: 0 }
}
@keyframes bottom {
from { --end: 0 }
to { --end: 1 }
}Code language: CSS (css)
We also need another animation that shows our scrollbar when the content overflows the container. We will use the same values for both from and to, since it only triggers when there is enough content to scroll.
@keyframes bar {
from, to {
--opacity: 1;
--events: auto;
}
}Code language: CSS (css)
Applying Styles
Now let’s apply all those animations with a linear timing function:
.bar {
animation-name: top, bottom, bar;
animation-timing-function: linear;
animation-timeline: --scrollbar;
}Code language: CSS (css)
Transitioning CSS variables based on scroll position can help us move the bar from top to bottom. But we also need the bar size to reflect the container’s total scroll amount. This means the top end of the bar starts at 0 but finishes before it reaches 100%. We can achieve this effect using animation-range. If we “pretend” there is one more offsetHeight to scroll through, then our animation progress will stop at the right spot when we scroll our container all the way to the bottom.
But how can we tell the animation range to extend to this exact amount? Another cool CSS feature called Container Queries will help us this time! Our bars container matches the dimensions of the scroll container thanks to anchor positioning, so we can mark it as a size container:
.bars {
container-type: size;
}Code language: CSS (css)
Now its children have access to its size, and we can define animation-range to change from 0% to 100% + 100cqb. The same, but in reverse, from 0% - 100cqb to 100% will move the bottom end of the bar in sync, giving the bar proper height that reflects offsetHeight by scrollHeight ratio. We also need animation-fill-mode to hold on to the values at the very edges of the scroll.
.bar {
animation-range: 0% calc(100% + 100cqb), calc(0% - 100cqb) 100%;
animation-fill-mode: both;
}Code language: CSS (css)
The final step is to apply the resulting styles to the bar. We also need to be mindful of super-long scrolls – we do not want our bar to become too small for the user to see and interact with. We can use a combination of min-block-size and min() CSS function to keep positioning from squishing our bar under extreme conditions.
.bar {
min-block-size: 1rem;
inset-block-start: min(calc(100% - var(--start) * 100%), calc(100% - 1rem));
inset-block-end: calc(100% - var(--end) * 100%);
opacity: var(--opacity);
pointer-events: var(--events);
}Code language: CSS (css)
Implementing User Interaction
Since our scrollbar component is just a scroll container with a native scrollbar hidden, most user interactions work out of the box: the container becomes keyboard-focusable when it overflows, arrow keys and page up/down can scroll it as usual, the same for the mouse wheel or touchpad, and for mobile touch scrolling. The only remaining interaction we need to worry about is pointer drag. JavaScript implementation is pretty straightforward. Upon the pointerdown event we need to query a few things, namely pageX/pageY of an event and our scrollbar state, so we know the starting values. One thing we need to keep in mind here is scrollLeft becomes negative in right-to-left languages, so we need to check for a multiplier as well:
bar.addEventListener('pointerdown', ({ pageX, pageY }) => {
const { scrollTop, scrollLeft, offsetHeight, offsetWidth, scrollHeight, scrollWidth } = scrollbar;
const rtl = scrollbar.matches('[dir="rtl"] :scope') ? -1 : 1;
// …Code language: JavaScript (javascript)
Next, we need a pointermove event listener that would call scrollTo with behavior: 'instant' to override any scroll-behavior CSS rule that might exist up the DOM hierarchy:
const move = e => {
const top = vertical ? scrollTop + (e.pageY - pageY) / offsetHeight * scrollHeight : scrollTop;
const left = vertical ? scrollLeft : scrollLeft + rtl * (e.pageX - pageX) / offsetWidth * scrollWidth;
scrollbar.scrollTo({ top, left, behavior: 'instant' });
};Code language: JavaScript (javascript)
Then we need to wire up those events and make sure we remove event listeners once the drag action ends:
const end = () => {
document.removeEventListener('pointermove', move);
document.removeEventListener('pointerup', end);
document.removeEventListener('pointercancel', end);
document.removeEventListener('contextmenu', end);
}
document.addEventListener('pointermove', move);
document.addEventListener('pointerup', end);
document.addEventListener('pointercancel', end);
document.addEventListener('contextmenu', end);Code language: JavaScript (javascript)
Conclusion
That’s it! Now our scrollbar can be interacted with thanks to a few lines of basic JavaScript, and the rest is handled by CSS. You can check out the complete demo in the Pen below:
Practical reasons for considering this approach include:
- Design system uniformity across browsers and OS.
- Desire to keep scrollbars overlaid on all platforms (since
overflow: overlayremoval, it’s practically unattainable with native scrollbars on Windows). Or the other way around, if you have clearly defined edges inside the container and do not want scrollbars to overlay those on macOS. - Reducing visual noise with many scrollable containers at once
- Elaborate custom designs for a wow effect that your designers came up with, and now it’s upon you to implement.
That’s why we see custom scrollbars all over the web, from giants like Facebook, Discord, and Spotify to WYSIWYG editors like VS Code for the web or CodePen. Check out some more demos below to see examples of such behaviors:
you probably want to wrap most of these in an
@supportswrapper, for better browser support. In non nightly firefox (as of this comment firefox 155.0.1 ) the scrolling is extremely broken.Also, i feel like most of these can be done with just scrollbar-color and scrollbar-width?
It’s coming to Firefox in version 159 expected next month. In the last 3 examples at the end I tried to showcase things that are not possible with standard CSS scrollbar-color/scrollbar-width 🙂
If you’re not sure if what your building is accessible you should do that before you get paid to talk about it. As opposed to adding a disclaimer that this might not be usable by anyone doing web development.
While I’m a bit concerned about the accessibility of this, it was wild seeing this implementation with CSS mostly.
Thanks for the article!
I appreciate you qualify the accessibility risks. For readers, two posts might be useful to understand those risks:
The demo used in the conclusion of this post fails two WCAG 2.2 Success Criteria:
The demo also ignores the Inclusive Design Principles of providing a comparable experience, giving control, offering choice, and adding value. If authors provided a way to disable the custom scrollbars, that would go a long way.
Finally, it doesn’t work in Firefox. As others have noted.
All that said, the blood spatter example scrollbar looks pretty sweet.
Thank you for bringing solid arguments and articles. I have updated color in the demo, as for target size – I’ve explained in the article that we should use min-size to avoid that, I just used 16px because I didn’t know accessibility standard is 24px, thank you for bringing it up, updated the code.
I’m not sure what you mean by Inclusive Design Principles here, since neither default scrollbars give users any choice, nor standard scrollbar-color/scrollbar-width CSS props, but since this is just CSS – you can easily hide all this code under a class and allow people to toggle this class with a setting of “Use custom scrollbars” if you need it.
As for Firefox, it was about to release scroll-driven animations when this article was written but then pushed it back to version 159 coming next month. The purpose was to explore the API, not to cover graceful degradation, obviously this needs to be put behind @supports in real-life scenario since it’s a very new feature.
Your accessibility concerns are focused on the actual visual style of the bar, which is the responsibility of the designer and can happen if you customize scrollbar any other way, including plain old scrollbar-color. As an expert, do you envision any accessibility problems explored approach inherently has?