A customizable React scrollbar component built on top of Perfect Scrollbar, with plugin support and rich APIs.
Supports inertia scroll, load more, gradient indicators, highlight, spy, virtualization, and custom plugins.
π Codesandbox
- β‘ Lightweight and smooth scrolling, built on top of Perfect Scrollbar
- π¨ Fully customizable (size, height, width, direction, easing)
- π Plugin System
- Inertia / Momentum scroll
- ScrollSpy
- Highlight item
- Gradient indicators
- Custom "Load More" support
- Pull-to-refresh
- Progress bar
- Minimap
- π Exposed ref API for programmatic control
- π Many event callbacks for scroll control
- π Easy integration into lists, tables, dashboards
- π Virtualization Support (render thousands of items smoothly, like
react-window)
npm install react-neo-scrollbar-zimport "react-neo-scrollbar-z/build/styles.css";
import React, { useState, useEffect, useRef } from "react";
import Scrollbar, { loadMorePlugin, pullToRefreshPlugin } from "react-neo-scrollbar-z";
import type { IFScrollbarRefProps } from "react-neo-scrollbar-z";
export default function DemoApp() {
const scrollbarRef = useRef<IFScrollbarRefProps>(null);
const [posts, setPosts] = useState<any[]>([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
useEffect(() => {
const initial = Array.from({ length: 10 }, (_, i) => ({ id: i + 1, title: "Post " + (i+1) }));
setPosts(initial);
}, []);
const handleRefresh = async () => {
await new Promise(r => setTimeout(r, 1000));
const newPost = { id: Date.now(), title: "Refreshed " + Date.now() };
setPosts(prev => [newPost, ...prev]);
};
const handleLoadMore = async () => {
if (!hasMore) return false;
await new Promise(r => setTimeout(r, 1000));
const nextId = posts.length + 1;
const newPosts = Array.from({ length: 5 }, (_, i) => ({ id: nextId + i, title: "Loaded " + (nextId + i) }));
setPosts(prev => [...prev, ...newPosts]);
setPage(p => p + 1);
if (page >= 3) { setHasMore(false); return false; }
return true;
};
return (
<Scrollbar
ref={scrollbarRef}
maxHeight={400}
plugins={[
// pullToRefreshPlugin({ onRefresh: handleRefresh }),
// loadMorePlugin({ onLoadMore: handleLoadMore }),
]}
>
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</Scrollbar>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
width |
string / number | β | Fixed width |
height |
string / number | β | Fixed height |
maxWidth |
string / number | β | Max width |
maxHeight |
string / number | β | Max height |
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | β | Unique identifier |
children |
ReactNode | β | Content |
style |
CSSProperties | β | Custom styles |
direction |
"vertical" | "horizontal" | "both" | "vertical" | Scroll direction |
always |
boolean | false | Always show scrollbar |
autoHide |
boolean | true | Auto-hide scrollbar |
disableKeyboard |
boolean | false | Disable keyboard |
wheelStop |
boolean | false | Stop wheel propagation |
scrollLock |
boolean | false | Prevent body scroll |
| Prop | Type | Default | Description |
|---|---|---|---|
inertia |
boolean | false | Enable inertia |
inertiaFriction |
number | 0.92 | Friction factor |
smoothScroll |
boolean | true | Smooth animations |
scrollDuration |
number | 300 | Duration for programmatic scroll |
scrollEasing |
function | easeInOutQuad | Custom easing |
autoScrollBottom |
boolean | false | Auto-scroll bottom |
autoScrollToNewItem |
boolean | false | Auto-scroll new item |
preserveScroll |
boolean | false | Save/restore position |
scrollSaveKey |
string | β | Storage key |
initialScroll |
"top" | "bottom" | number | "top" | Initial scroll |
triggerScrollClick |
boolean | false | Click track to scroll |
| Prop | Type | Default | Description |
|---|---|---|---|
plugins |
IFScrollbarPlugin[] | [] | Attach plugins |
effectData |
unknown | β | Trigger re-render |
options |
PerfectScrollbar.Options | β | Native options |
refScroll |
MutableRefObject | β | Ref API |
| Callback | Params | Description |
|---|---|---|
onScroll |
{x, y} | Fires on scroll |
onScrollStart |
β | Fires on start |
onScrollStop |
β | Fires on stop |
onReachTop |
β | Reached top |
onReachBottom |
β | Reached bottom |
onReachLeft |
β | Reached left |
onReachRight |
β | Reached right |
onReachThreshold |
percent: number | Threshold crossed |
onOverflowChange |
(x: boolean, y: boolean) | Overflow change |
scrollToX(x: number, duration?: number, easing?: ScrollEasingFn)
scrollToY(y: number, duration?: number, easing?: ScrollEasingFn)
scrollToTop(duration?: number, easing?: ScrollEasingFn)
scrollToBottom(duration?: number, easing?: ScrollEasingFn)
scrollToIndex(index: number, duration?: number, easing?: ScrollEasingFn)
scrollToElement(el: HTMLElement, duration?: number, easing?: ScrollEasingFn)
scrollToPercent(yPercent: number, xPercent?: number, duration?: number, easing?: ScrollEasingFn)
update()
destroy()
getScrollbar()
getScrollElement()export interface IFScrollbarPlugin {
mountPosition?: "inside" | "outside" | "before";
onInit?: (api: IFScrollbarRefProps, el: HTMLElement) => void;
onScroll?: (el: HTMLElement) => void;
onDestroy?: () => void;
render?: () => React.ReactNode;
onScrollStart?: (el: HTMLElement) => void;
onScrollStop?: (el: HTMLElement) => void;
onReachTop?: () => void;
onReachBottom?: () => void;
onReachLeft?: () => void;
onReachRight?: () => void;
onReachThreshold?: (percent: number) => void;
}- bounceEffectPlugin
- bounceHighlightPlugin
- gradientIndicatorPlugin
- highlightItemPlugin
- inertiaPlugin
- loadMorePlugin
- minimapPlugin
- pullToRefreshPlugin
- progressBarPlugin
- scrollSpyPlugin
- snapToItemPlugin
VirtualList allows you to render tens of thousands of rows efficiently by only rendering visible items in the viewport.
MIT