Virtual List
Windowed scrolling list that keeps render cost flat regardless of dataset size — essential for long mobile lists.
Basic Usage
10,000-row virtual list
Only the rows in the visible window are rendered, so scrolling stays smooth even with very long lists.
import { VirtualList } from 'fluxo-ui';
<VirtualList
items={items}
itemHeight={48}
height={400}
renderItem={(item) => <div>{item.name}</div>}
keyExtractor={(item) => item.id}
/>Variants
variant="plain"
No separators — pair with custom border styling on each row.
variant="divided"
A subtle hairline between rows.
variant="card"
Each row rendered as a rounded card with breathing room.
<VirtualList variant="divided" ... />
<VirtualList variant="card" ... />Infinite Loading
Infinite scroll via onEndReached
Fires when scrolled within endReachedThreshold of the bottom. Pair with VirtualList for unbounded feeds.
<VirtualList
items={items}
itemHeight={48}
height={360}
endReachedThreshold={150}
onEndReached={() => loadMore()}
renderItem={(item) => <Row item={item} />}
/>Import
import { VirtualList } from 'fluxo-ui';
import type { VirtualListProps, VirtualListHandle, VirtualListVariant } from 'fluxo-ui';Props
itemsreqT[]Source array. Only the rows in the visible window are rendered.
itemsreqT[]Source array. Only the rows in the visible window are rendered.
itemHeightnumber | (index: number, item: T) => numberRow height. Pass a number for uniform rows or a function for per-row heights. When omitted, estimatedItemHeight is used.
itemHeightnumber | (index: number, item: T) => numberRow height. Pass a number for uniform rows or a function for per-row heights. When omitted, estimatedItemHeight is used.
renderItemreq(item: T, index: number) => ReactNodeRenderer for each row.
renderItemreq(item: T, index: number) => ReactNodeRenderer for each row.
keyExtractor(item: T, index: number) => KeyOptional key extractor for stable row identity.
keyExtractor(item: T, index: number) => KeyOptional key extractor for stable row identity.
overscannumber"4"Number of rows to render before/after the visible window.
overscannumber"4"Number of rows to render before/after the visible window.
heightstring | number"'100%'"CSS height of the scrollable container.
heightstring | number"'100%'"CSS height of the scrollable container.
estimatedItemHeightnumber"56"Fallback row height when itemHeight is not provided.
estimatedItemHeightnumber"56"Fallback row height when itemHeight is not provided.
variant'plain' | 'divided' | 'card'"'plain'"Visual style. 'divided' draws a separator between rows; 'card' renders each row in a card with margin.
variant'plain' | 'divided' | 'card'"'plain'"Visual style. 'divided' draws a separator between rows; 'card' renders each row in a card with margin.
emptyStateReactNodeOptional content rendered when items is empty.
emptyStateReactNodeOptional content rendered when items is empty.
onEndReached() => voidFired when the scroll position is within endReachedThreshold of the bottom. Pairs with infinite loading.
onEndReached() => voidFired when the scroll position is within endReachedThreshold of the bottom. Pairs with infinite loading.
endReachedThresholdnumber"200"Pixels from the bottom at which onEndReached should fire.
endReachedThresholdnumber"200"Pixels from the bottom at which onEndReached should fire.
classNamestringAdditional CSS class for the scroller.
classNamestringAdditional CSS class for the scroller.
ariaLabelstringAccessible name for the list landmark.
ariaLabelstringAccessible name for the list landmark.
Features
Windowed rendering
Only rows visible in the viewport (plus overscan) are mounted.
Fixed or dynamic heights
Pass a number for uniform rows or a function for per-row heights.
Imperative scroll API
scrollToIndex / scrollToOffset / getScrollOffset via ref.
Infinite loading hook
onEndReached fires when the user nears the bottom — perfect for paged feeds.
Three variants
plain, divided (subtle separators), and card (rounded rows).
Theme-aware
Uses --eui-* variables so it adopts every brand theme and dark mode.