(
Row {index.toLocaleString()}
)}
/>
)
}
```
### Custom Scrollbar
```tsx
import type { HTMLProps } from "react"
import ScrollBar, { type RenderElement } from "@better-scrollbar/react"
import "@better-scrollbar/react/styles/ScrollBar.less"
const renderThumb: RenderElement> = (props) => (
)
export default function CustomScrollbar() {
return (
Large content
)
}
```
### React Props
| Prop | Type | Default | Description |
| -------------------------------- | ------------------------------------ | -------------- | --------------------------------------------------------------------------------------------------------- |
| `children` | `ReactNode` | — | Items for children mode |
| `itemCount` | `number` | — | Total item count for indexed rendering |
| `renderItem` | `(index: number) => ReactElement` | — | Lazy row renderer. When `itemCount` and `renderItem` are both provided, indexed rendering takes priority. |
| `itemKey` | `(index: number) => Key` | `index` | Stable key generator for indexed items |
| `isVirtual` | `boolean` | `true` | Enables virtual rendering. Falls back to virtual when item count exceeds `maxRenderedItems`. |
| `width` | `number` | — | Scroll viewport width (px) |
| `height` | `number` | — | Scroll viewport height (px) |
| `className` | `string` | — | Class name for the outer container |
| `style` | `CSSProperties` | — | Inline style for the outer container |
| `itemHeight` | `number` | `20` | Default row height |
| `estimatedItemHeight` | `number` | `itemHeight` | Estimated height for unmeasured rows |
| `heightCacheLimit` | `number` | `50000` | Max measured heights retained (LRU). Pass `Infinity` to disable eviction. |
| `overscan` | `number \| OverscanConfig` | `1` | Extra item count or full overscan config |
| `adaptiveOverscan` | `boolean \| AdaptiveOverscanOptions` | `false` | Direction-aware dynamic overscan |
| `maxRenderedItems` | `number` | `500` | DOM item cap. Visible range is always preserved. Pass `Infinity` to disable. |
| `scrollSeek` | `boolean \| ScrollSeekOptions` | `false` | Placeholder mode during fast scrolling |
| `scrollMode` | `"controlled" \| "native"` | `"controlled"` | Wheel input strategy |
| `maintainVisibleContentPosition` | `boolean` | `true` | Anchor visible item on data/measurement changes |
| `followOutput` | `boolean \| FollowOutputOptions` | `false` | Pin to bottom on append |
| `preserveItemState` | `boolean` | `false` | Keep non-visible children mounted (hidden) |
| `stickyIndices` | `number[]` | — | Sticky item indexes |
| `groupCounts` | `number[]` | — | Per-group sizes for sticky group headers |
| `maxBrowserScrollHeight` | `number` | `10000000` | Physical scroll height cap |
| `prefixCls` | `string` | `"scroll-bar"` | CSS class prefix |
| `scrollBarSize` | `number` | `6` | Scrollbar thickness |
| `scrollBarHidden` | `boolean` | `false` | Hides scrollbars |
| `scrollBarAutoHideTimeout` | `number` | `1000` | Auto-hide delay (ms) |
| `onScroll` | `(state: ScrollState) => void` | — | Scroll state callback |
| `onScrollStart` | `() => void` | — | Scroll start callback |
| `onScrollEnd` | `() => void` | — | Scroll end callback |
| `onItemsRendered` | `(info: ItemsRenderedInfo) => void` | — | Rendered range callback |
| `renderView` | `RenderElement` | built-in `div` | Custom scroll view wrapper |
| `renderTrackHorizontal` | `RenderElement` | built-in | Custom horizontal track |
| `renderTrackVertical` | `RenderElement` | built-in | Custom vertical track |
| `renderThumbHorizontal` | `RenderElement` | built-in | Custom horizontal thumb |
| `renderThumbVertical` | `RenderElement` | built-in | Custom vertical thumb |
### React Ref API
```ts
interface VirtualScrollBarRef {
scrollTo(offset: { x: number; y: number }): void
getScrollState(): ScrollState
resizeObserver(
callback: (
size: Pick
) => void
): void
}
```
| Method | Description |
| -------------------- | ---------------------------------------------------------------------- |
| `scrollTo({ x, y })` | Scrolls to logical coordinates |
| `getScrollState()` | Returns latest scroll state (reads from internal ref, not React state) |
| `resizeObserver(cb)` | Fires callback immediately with current dimensions |
### React Exported Types
```ts
import type {
VirtualScrollBarProps,
VirtualScrollBarRef,
RenderElement,
RenderItem,
ScrollSeekPlaceholder,
ScrollSeekOptions,
// Re-exported from core:
ScrollState,
ScrollOffset,
ItemsRenderedInfo,
AdaptiveOverscanOptions,
OverscanConfig,
OverscanRange,
FollowOutputOptions
} from "@better-scrollbar/react"
```
---
## Vue 3 — `@better-scrollbar/vue`
### Basic Usage (Vue)
```vue
Row {{ index }}
```
### Vue Props
| Prop | Type | Default | Description |
| --------------------- | -------------------------- | -------------- | ---------------------------------------- |
| `itemCount` | `number` | **required** | Total number of items |
| `estimatedItemHeight` | `number` | `20` | Estimated height for unmeasured rows |
| `height` | `number` | **required** | Viewport height (px) |
| `width` | `number` | — | Viewport width (px). Defaults to `100%`. |
| `overscan` | `number \| OverscanConfig` | `1` | Extra item count or overscan config |
| `maxRenderedItems` | `number` | `500` | Rendered item cap |
| `prefixCls` | `string` | `"scroll-bar"` | CSS class prefix |
### Vue Events
| Event | Payload | Description |
| --------------- | ------------------- | ----------------------------------- |
| `scroll` | `ScrollState` | Emitted on scroll |
| `scrollStart` | — | Emitted when scrolling starts |
| `scrollEnd` | — | Emitted when scrolling ends |
| `itemsRendered` | `ItemsRenderedInfo` | Emitted when rendered ranges change |
### Vue Slots
| Slot | Scope | Description |
| --------- | ------------------- | --------------------------------------- |
| `default` | `{ index: number }` | Scoped slot for rendering each item row |
### Vue Exposed Methods
Access via template ref (`ref="scrollbar"`):
```ts
interface BScrollBarExposed {
scrollTo(offset: Partial): void
getScrollState(): ScrollState
}
```
### Vue Composables
#### `useScrollBar(options): UseScrollBarReturn`
Core composable managing scroll state and virtual range computation.
```ts
interface UseScrollBarOptions {
itemCount: MaybeRef
estimatedItemHeight?: MaybeRef
height: MaybeRef
width?: MaybeRef
overscan?: MaybeRef
maxRenderedItems?: MaybeRef
}
interface UseScrollBarReturn {
heightIndex: Ref
scrollState: Ref
range: ComputedRef
visibleItems: ComputedRef
scrollTo: (offset: Partial) => void
handleScroll: (event: Event) => void
getScrollState: () => ScrollState
}
```
#### `useHeights(options)`
Height index management with reactive measurement tracking.
```ts
interface UseHeightsOptions {
itemCount: MaybeRef
estimatedItemHeight: MaybeRef
heightCacheLimit?: MaybeRef
}
```
Returns `{ heightIndex, totalHeight, setMeasuredHeight, deleteMeasuredHeight, measureElement, reset }`.
#### `useResizeObserver(target: Ref)`
Tracks element size changes. Returns `{ size, updateSize, stopObserver, startObserver }`.
### Vue Exported Types
```ts
import type {
BScrollBarProps,
BScrollBarExposed,
UseScrollBarOptions,
UseScrollBarReturn,
UseHeightsOptions,
UseResizeObserverSize,
MaybeRef,
ScrollState,
ScrollOffset,
ItemsRenderedInfo,
OverscanConfig
} from "@better-scrollbar/vue"
```
---
## Core — `@better-scrollbar/core`
### Virtual Height Index
The central data structure for virtual scrolling — a block-based Fenwick tree
that maps item indexes to pixel offsets.
```ts
import { createVirtualHeightIndex, createVirtualHeightIndexStore } from "@better-scrollbar/core"
```
#### `createVirtualHeightIndexStore(options): VirtualHeightIndexStore`
```ts
interface VirtualHeightIndexOptions {
itemCount: number
estimatedItemHeight: number
measuredHeights?: Map
blockSize?: number // default 512
maxMeasuredItems?: number // default Infinity
}
interface VirtualHeightIndexStore extends VirtualHeightIndex {
setMeasuredHeight(index: number, height: number): VirtualHeightIndexStore
deleteMeasuredHeight(index: number): VirtualHeightIndexStore
reset(options: VirtualHeightIndexOptions): VirtualHeightIndexStore
}
interface VirtualHeightIndex {
totalHeight: number
getOffset(index: number): number
getRange(options: VirtualRangeOptions): VirtualRangeResult
}
```
#### `VirtualRangeOptions`
```ts
interface VirtualRangeOptions {
scrollOffset: number
viewportSize: number
overscan: number | VirtualOverscanRange
overscanPixels?: number | VirtualOverscanRange
maxItems?: number
}
```
#### `VirtualRangeResult`
```ts
interface VirtualRangeResult {
scrollHeight: number
start: number
end: number
visibleStartIndex: number
visibleEndIndex: number
offset: number
}
```
### Utility Functions
| Function | Signature | Description |
| ---------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ |
| `getStickyIndicesFromGroups` | `(groupCounts: number[]) => number[]` | Derives sticky header indexes from group sizes |
| `getSpinSize` | `(containerSize: number, scrollRange: number) => number` | Calculates scrollbar thumb size |
| `raf` | `(callback: () => void, times?: number) => number` | `requestAnimationFrame` wrapper with chaining and `raf.cancel(id)` |
| `isDOM` | `(node: unknown) => node is HTMLElement \| SVGElement` | Type guard for DOM elements |
| `getPageXY` | `(event, horizontal?) => number` | Extracts page coordinate from mouse/touch events |
---
## Shared Types
These types are defined in `@better-scrollbar/core` and re-exported by both
adapters.
### `ScrollState`
| Field | Type | Description |
| -------------- | --------- | -------------------------------------- |
| `x` | `number` | Horizontal scroll offset |
| `y` | `number` | Vertical scroll offset |
| `scrollWidth` | `number` | Full scrollable content width |
| `scrollHeight` | `number` | Full scrollable content height |
| `clientWidth` | `number` | Viewport width |
| `clientHeight` | `number` | Viewport height |
| `isScrolling` | `boolean` | Whether a scroll interaction is active |
### `ScrollOffset`
| Field | Type | Description |
| ----- | -------- | ----------------- |
| `x` | `number` | Horizontal offset |
| `y` | `number` | Vertical offset |
### `ItemsRenderedInfo`
| Field | Type | Description |
| ------------------- | -------- | ------------------------------------ |
| `startIndex` | `number` | First rendered index (with overscan) |
| `endIndex` | `number` | Last rendered index (with overscan) |
| `visibleStartIndex` | `number` | First visible index |
| `visibleEndIndex` | `number` | Last visible index |
### `AdaptiveOverscanOptions`
| Field | Type | Default | Description |
| ---------------- | -------- | -------------- | ---------------------------------- |
| `min` | `number` | base overscan | Lower bound |
| `max` | `number` | `overscan + 4` | Upper bound |
| `velocityFactor` | `number` | `0.02` | Scroll distance multiplier |
| `timeFactor` | `number` | `0.15` | Scroll velocity multiplier (px/ms) |
### `OverscanRange`
| Field | Type | Description |
| -------- | -------- | -------------------------------------- |
| `before` | `number` | Extra items/pixels before the viewport |
| `after` | `number` | Extra items/pixels after the viewport |
### `OverscanConfig`
| Field | Type | Default | Description |
| ---------- | ------------------------------------ | ------- | ------------------ |
| `items` | `number` | `1` | Extra item count |
| `pixels` | `number \| OverscanRange` | — | Pixel-based buffer |
| `adaptive` | `boolean \| AdaptiveOverscanOptions` | `false` | Dynamic overscan |
### `FollowOutputOptions`
| Field | Type | Default | Description |
| ----------- | -------- | ------- | ------------------------------------ |
| `threshold` | `number` | `1` | Pixel threshold for bottom detection |
### `ScrollSeekOptions`
| Field | Type | Default | Description |
| ----------------------- | --------------------------- | --------------------------- | ------------------------------------------ |
| `velocityThreshold` | `number` | `2` | Velocity (px/ms) to enter placeholder mode |
| `exitVelocityThreshold` | `number` | half of `velocityThreshold` | Velocity to exit placeholder mode |
| `placeholder` | `Placeholder` | — | Lightweight placeholder renderer |
| `onChange` | `(active: boolean) => void` | — | Mode toggle callback |
---
## Performance Notes
- **Fenwick tree index** — Dynamic heights are tracked with a block-based
Fenwick index. Single-row resize updates cost O(log n) instead of rebuilding
the full height table.
- **Fixed-height fast path** — Fixed-height lists skip the dynamic index.
Offsets are calculated as `index * estimatedItemHeight`.
- **Shared ResizeObserver** — Row measurement uses one shared `ResizeObserver`.
Observer callbacks prefer `contentRect.height` and fall back to `offsetHeight`.
- **Batched updates** — High-frequency wheel and drag input writes to internal
refs first, then batches React state updates through `requestAnimationFrame`.
- **Massive ranges** — When logical height exceeds the browser-safe physical
height, DOM `scrollTop` is re-based into a local logical window.
- **Native scroll pipeline** — `scrollMode="native"` uses browser scrolling
when possible. Compressed massive ranges keep controlled wheel handling for
logical precision.
- **Adaptive overscan** — `adaptiveOverscan` calculates direction-aware
pre-rendering from scroll distance, event timing, and `devicePixelRatio`.
- **Pixel overscan** — `overscanPixels` / `OverscanConfig.pixels` controls
pre-rendering by pixels instead of row count.
- **DOM cap** — `maxRenderedItems` protects overscan, adaptive overscan,
`isVirtual={false}`, and `preserveItemState` from mounting too many DOM nodes.
- **Height cache limit** — `heightCacheLimit` provides LRU eviction for measured
heights, avoiding unbounded cache growth.
- **Scroll seek** — Placeholders keep list structure stable during fast
scrolling; real rows restore when velocity drops. Placeholder rows do not
participate in height measurement.
- **Accessibility** — The wrapper uses `role="list"`, rendered items use
`role="listitem"` with `aria-posinset` and `aria-setsize`.