@aiquants/virtualscroll
v1.15.0
Published
High-performance virtual scrolling component for React with variable item heights
Readme
@aiquants/virtualscroll
High-performance virtual scrolling component for React with variable item heights using Fenwick Tree optimization.
Features
- ⚡ High Performance: Optimized for thousands of items with O(log n) operations
- 📐 Variable Heights: Support for items with different heights
- 🎯 Precise Scrolling: Accurate scroll positioning and smooth navigation
- 📱 Touch Support: Full support for touch devices
- 🎨 Customizable: Flexible styling and theming options
- 🌀 Ultrafast Tap Scroll: Adaptive tap scroll circle that scales speed up to 120× for massive datasets
- 🔧 TypeScript: Full TypeScript support with comprehensive type definitions
Installation
npm install @aiquants/virtualscroll
# or
yarn add @aiquants/virtualscroll
# or
pnpm add @aiquants/virtualscrollBasic Usage
import { VirtualScroll } from '@aiquants/virtualscroll'
import { useCallback } from 'react'
type Item = {
id: number
text: string
height: number
}
const items: Item[] = Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`,
height: Math.floor(Math.random() * 50) + 30, // Random height between 30-80px
}))
function App() {
const getItem = useCallback((index: number) => items[index], [])
const getItemHeight = useCallback((index: number) => items[index].height, [])
return (
<div style={{ height: '400px', width: '100%' }}>
<VirtualScroll
itemCount={items.length}
getItem={getItem}
getItemHeight={getItemHeight}
viewportSize={400}
overscanCount={5}
className="border border-gray-300"
>
{(item, index) => (
<div
key={item.id}
style={{
height: item.height,
padding: '8px',
borderBottom: '1px solid #eee',
display: 'flex',
alignItems: 'center',
}}
>
<span>#{index}: {item.text}</span>
</div>
)}
</VirtualScroll>
</div>
)
}API Reference
VirtualScroll Props
| Prop | Type | Required | Description |
| ------ | ------ | ---------- | ------------- |
| children | (item: T, index: number) => ReactNode | ✅ | Render function for items |
| itemCount | number | ✅ | Total number of items |
| getItem | (index: number) => T | ✅ | Function to get item at index |
| getItemHeight | (index: number) => number | ✅ | Function to get item height |
| viewportSize | number | ✅ | Height of the visible area |
| overscanCount | number | ❌ | Number of items to render outside viewport (default: 5) |
| className | string | ❌ | CSS class name |
| onScroll | (position: number, totalHeight: number) => void | ❌ | Scroll event handler |
| onRangeChange | (range: VirtualScrollRange) => void | ❌ | Range change handler |
| background | ReactNode | ❌ | Background element |
| initialScrollIndex | number | ❌ | Initial scroll index |
| initialScrollOffset | number | ❌ | Initial scroll offset |
| contentInsets | ScrollPaneContentInsets | ❌ | Insets for the content area |
| callbackThrottleMs | number | ❌ | Throttle time for scroll callbacks (default: 5ms) |
| onItemFocus | (index: number) => void | ❌ | Callback when an item is focused |
| scrollBarOptions | VirtualScrollScrollBarOptions | ❌ | Options for the scrollbar |
| behaviorOptions | VirtualScrollBehaviorOptions | ❌ | Options for scrolling behavior |
VirtualScrollScrollBarOptions
| Property | Type | Description |
| ---------- | ------ | ------------- |
| width | number | Width of the scrollbar |
| enableThumbDrag | boolean | Enable dragging the scrollbar thumb |
| enableTrackClick | boolean | Enable clicking the scrollbar track |
| enableArrowButtons | boolean | Enable arrow buttons on the scrollbar |
| enableScrollToTopBottomButtons | boolean | Enable "Scroll to Top" and "Scroll to Bottom" buttons |
| renderThumbOverlay | (props: ScrollBarThumbOverlayRenderProps) => ReactNode | Render prop to anchor custom UI near the scrollbar thumb |
| tapScrollCircleOptions | ScrollBarTapCircleOptions | Customization for the auxiliary tap scroll circle |
VirtualScrollBehaviorOptions
| Property | Type | Description |
| ---------- | ------ | ------------- |
| enablePointerDrag | boolean | Enable dragging the content area to scroll |
| enableKeyboardNavigation | boolean | Enable keyboard navigation (default: true) |
| wheelSpeedMultiplier | number | Multiplier for mouse wheel scrolling speed |
| inertiaOptions | ScrollPaneInertiaOptions | Physics tuning for drag inertia |
| clipItemHeight | boolean | Whether to clip item height (default: false) |
| resetOnGetItemHeightChange | boolean | Whether to reset internal height cache when getItemHeight changes (default: false) |
VirtualScrollHandle Methods
| Method | Type | Description |
| -------- | ------ | ------------- |
| scrollTo | (position: number) => void | Scroll to specific position |
| scrollToIndex | (index: number, options?: { align?: "top" \| "bottom" \| "center"; offset?: number }) => void | Scroll to specific item index with optional alignment and offset |
| getScrollPosition | () => number | Get current scroll position |
| getContentSize | () => number | Get total content size |
| getViewportSize | () => number | Get viewport size |
| focusItemAtIndex | (index: number, options?: { ensureVisible?: boolean }) => void | Focus item at specific index |
| getRange | () => VirtualScrollRange | Get current range information |
VirtualScrollRange
| Property | Type | Description |
| ---------- | ------ | ------------- |
| renderingStartIndex | number | Index of the first item being rendered (including overscan) |
| renderingEndIndex | number | Index of the last item being rendered (including overscan) |
| visibleStartIndex | number | Index of the first fully or partially visible item |
| visibleEndIndex | number | Index of the last fully or partially visible item |
| scrollPosition | number | Current scroll position in pixels |
| totalHeight | number | Total height of the scroll content |
Advanced Usage
With Ref and Scroll Control
import { ScrollBarThumbOverlayRenderProps, VirtualScroll, VirtualScrollHandle } from '@aiquants/virtualscroll'
import { useCallback, useRef, useState } from 'react'
const items = Array.from({ length: 100000 }, (_, index) => ({
id: index,
text: `Item ${index}`,
height: (index % 20) * 2 + 30,
}))
const getItem = (index: number) => items[index]
const getItemHeight = (index: number) => items[index].height
function AdvancedExample() {
const virtualScrollRef = useRef<VirtualScrollHandle>(null)
const [visibleStartIndex, setVisibleStartIndex] = useState(0)
const scrollToTop = () => {
virtualScrollRef.current?.scrollTo(0)
}
const scrollToIndex = (index: number) => {
// Scroll to item 500, aligning it to the center of the viewport
virtualScrollRef.current?.scrollToIndex(index, { align: "center" })
}
const handleRangeChange = useCallback((
_renderingStartIndex: number,
_renderingEndIndex: number,
visibleStart: number,
_visibleEndIndex: number,
_scrollPosition: number,
_totalHeight: number,
) => {
setVisibleStartIndex(visibleStart)
}, [])
const renderThumbOverlay = useCallback((props: ScrollBarThumbOverlayRenderProps) => {
if (!(props.isDragging || props.isTapScrollActive)) {
return null
}
const activeItem = items[visibleStartIndex]
const label = activeItem ? activeItem.text : `Item ${visibleStartIndex}`
return (
<div
className="pointer-events-none absolute flex items-center"
style={
props.orientation === 'vertical'
? { top: props.thumbCenter, left: -14, transform: 'translate(-100%, -50%)' }
: { left: props.thumbCenter, top: -14, transform: 'translate(-50%, -100%)' }
}
>
<div className="rounded-full border border-slate-200 bg-white px-2 py-1 text-xs font-medium text-slate-700 shadow-md">
{label}
</div>
</div>
)
}, [visibleStartIndex])
return (
<div>
<div>
<button onClick={scrollToTop}>Scroll to Top</button>
<button onClick={() => scrollToIndex(500)}>Scroll to Item 500</button>
</div>
<VirtualScroll
ref={virtualScrollRef}
itemCount={items.length}
getItem={getItem}
getItemHeight={getItemHeight}
viewportSize={400}
onRangeChange={handleRangeChange}
scrollBarOptions={{
renderThumbOverlay: renderThumbOverlay
}}
>
{(item, index) => <ItemComponent item={item} index={index} />}
</VirtualScroll>
</div>
)
}Custom Scrollbar Styling
<VirtualScroll
itemCount={items.length}
getItem={getItem}
getItemHeight={getItemHeight}
viewportSize={400}
className="custom-virtual-scroll"
>
{(item, index) => <ItemComponent item={item} index={index} />}
</VirtualScroll>
<style>
.custom-virtual-scroll .scrollbar {
background-color: #f0f0f0;
}
.custom-virtual-scroll .scrollbar-thumb {
background-color: #007acc;
border-radius: 4px;
}
</style>Tap Scroll Circle Configuration
The auxiliary tap scroll circle can replace the native scrollbar for large datasets while remaining easy to control:
- Adaptive speed scaling automatically ramps up to a
120×multiplier asitemCountgrows (trillions supported). - Manual overrides let you clamp or extend speed via
maxSpeedMultiplierwhen you need deterministic behavior. - Exponential capping enables distance-sensitive ceilings with
maxSpeedCurve, blending gentle near-threshold drag with high-speed travel at extended distances. UseeasedOffsetto slightly raise the entry-level speed without changing the ceiling. - Full layout control (
size,offsetX,offsetY) keeps the circle accessible on both desktop and touch devices. - Visibility tuning exposes an
opacityknob so you can match subdued or high-contrast UI themes.
import { VirtualScroll } from "@aiquants/virtualscroll"
const items = Array.from({ length: 1_000_000_000_000 }, (_, index) => ({
id: index,
text: `Record ${index}`,
height: 42,
}))
export function UltraFastExample() {
return (
<VirtualScroll
itemCount={items.length}
getItem={(index) => items[index]}
getItemHeight={() => 42}
viewportSize={480}
scrollBarOptions={{
tapScrollCircleOptions: {
maxSpeedMultiplier: 80, // Optional: override adaptive speed when needed
maxSpeedCurve: {
exponentialSteepness: 6,
exponentialScale: 80,
easedOffset: 0.1,
},
offsetX: -96,
opacity: 0.85,
}
}}
>
{(item) => <div style={{ height: item.height }}>{item.text}</div>}
</VirtualScroll>
)
}Performance Tips
- Memoize callback functions: Use
useCallbackforgetItemandgetItemHeight - Optimize item rendering: Memoize item components when possible
- Adjust overscan count: Balance between smooth scrolling and memory usage
- Consider item height consistency: More consistent heights provide better performance
Browser Support
- Chrome 88+
- Firefox 87+
- Safari 14+
- Edge 88+
License
MIT
