Maximum performance for extremely large lists.<br/> Flexible, and actively maintained React library that excels with high-performance, feature-rich virtualized lists—including grouping, sticky headers, snapping, animations, and both scroll directions. Wh
bash
npm i rcx-virtual-list
`
Examples
$3
!preview
Code:
`tsx
const horizontalItemRendererFactory = (): VirtualListItemRenderer => ({ data, config }) => {
if (!data) {
return;
}
return {data?.name}
};
const MAX_ITEMS = 10000;
const HORIZONTAL_ITEMS: IVirtualListCollection = [];
for (let i = 0, l = MAX_ITEMS; i < l; i++) {
const id = i + 1;
HORIZONTAL_ITEMS.push({ id, name: ${id} });
}
function App() {
const [horizontalItems] = useState([...HORIZONTAL_ITEMS]);
const onItemClick = (item: IRenderVirtualListItem) => {
console.info( Click: (ID: ${item.id}) Item ${item.data.name});
};
return itemSize={54} bufferSize={50} onItemClick={onItemClick}/>
}
`
$3
!preview
Code:
`tsx
const horizontalGroupItemRendererFactory = (): VirtualListItemRenderer => ({ data, config }) => {
if (!data) {
return;
}
switch (data['type']) {
case 'group-header': {
return {data?.name}
}
case 'item': {
return {data?.name}
}
}
};
const MAX_ITEMS = 10000, GROUP_NAMES = ['A', 'B', 'C', 'D', 'E'];
const getGroupName = () => {
return GROUP_NAMES[Math.floor(Math.random() * GROUP_NAMES.length)];
};
const HORIZONTAL_GROUP_ITEMS: IVirtualListCollection = [],
HORIZONTAL_GROUP_ITEMS_STICKY_MAP: IVirtualListStickyMap = {};
for (let i = 0, l = MAX_ITEMS; i < l; i++) {
const id = i + 1, type = i === 0 || Math.random() > .895 ? 'group-header' : 'item';
HORIZONTAL_GROUP_ITEMS.push({ id, type, name: type === 'group-header' ? getGroupName() : ${id} });
HORIZONTAL_GROUP_ITEMS_STICKY_MAP[id] = type === 'group-header' ? 1 : 0;
}
function App() {
const [horizontalGroupItems] = useState([...HORIZONTAL_GROUP_ITEMS]);
const [horizontalGroupItemsStickyMap] = useState({ ...HORIZONTAL_GROUP_ITEMS_STICKY_MAP });
return items={horizontalGroupItems} itemSize={54} bufferSize={50} snap={true} stickyMap={horizontalGroupItemsStickyMap} />
}
`
$3
!preview
Code:
`tsx
const itemRendererFactory = (): VirtualListItemRenderer => ({ data, config }) => {
if (!data) {
return;
}
return {data?.name}
};
const MAX_ITEMS = 10000;
const ITEMS: IVirtualListCollection = [];
for (let i = 0, l = MAX_ITEMS; i < l; i++) {
const id = i + 1;
ITEMS.push({ id, name: Item: ${id} });
}
function App() {
const [verticalItems] = useState([...ITEMS]);
const onItemClick = (item: IRenderVirtualListItem) => {
console.info( Click: (ID: ${item.id}) Item ${item.data.name});
};
return items={verticalItems} itemSize={40} bufferSize={50} onItemClick={onItemClick} />
}
`
$3
#### Without snapping
!preview
Code:
`tsx
const groupItemRendererFactory = (): VirtualListItemRenderer => ({ data, config }) => {
if (!data) {
return;
}
switch (data['type']) {
case 'group-header': {
return {data?.name}
}
case 'item': {
return {data?.name}
}
}
};
const MAX_ITEMS = 10000, GROUP_ITEMS: IVirtualListCollection = [],
GROUP_ITEMS_STICKY_MAP: IVirtualListStickyMap = {};
let groupIndex = 0;
for (let i = 0, l = MAX_ITEMS; i < l; i++) {
const id = i + 1, type = i === 0 || Math.random() > .895 ? 'group-header' : 'item';
if (type === 'group-header') {
groupIndex++;
}
GROUP_ITEMS.push({ id, type, name: type === 'group-header' ? Group ${groupIndex} : Item: ${id} });
GROUP_ITEMS_STICKY_MAP[id] = type === 'group-header' ? 1 : 0;
}
function App() {
const [groupItems] = useState([...GROUP_ITEMS]);
const [groupItemsStickyMap] = useState({ ...GROUP_ITEMS_STICKY_MAP });
const onItemClick = (item: IRenderVirtualListItem) => {
console.info( Click: (ID: ${item.id}) Item ${item.data.name});
};
return stickyMap={groupItemsStickyMap} itemSize={40} onItemClick={onItemClick} />
}
`
#### With snapping
!preview
Code
`tsx
const groupItemRendererFactory = (): VirtualListItemRenderer => ({ data, config }) => {
if (!data) {
return;
}
switch (data['type']) {
case 'group-header': {
return {data?.name}
}
case 'item': {
return {data?.name}
}
}
};
const MAX_ITEMS = 10000, GROUP_ITEMS: IVirtualListCollection = [],
GROUP_ITEMS_STICKY_MAP: IVirtualListStickyMap = {};
let groupIndex = 0;
for (let i = 0, l = MAX_ITEMS; i < l; i++) {
const id = i + 1, type = i === 0 || Math.random() > .895 ? 'group-header' : 'item';
if (type === 'group-header') {
groupIndex++;
}
GROUP_ITEMS.push({ id, type, name: type === 'group-header' ? Group ${groupIndex} : Item: ${id} });
GROUP_ITEMS_STICKY_MAP[id] = type === 'group-header' ? 1 : 0;
}
function App() {
const [groupItems] = useState([...GROUP_ITEMS]);
const [groupItemsStickyMap] = useState({ ...GROUP_ITEMS_STICKY_MAP });
const onItemClick = (item: IRenderVirtualListItem) => {
console.info( Click: (ID: ${item.id}) Item ${item.data.name});
};
return stickyMap={groupItemsStickyMap} itemSize={40} snap={true} onItemClick={onItemClick} />
}
`
$3
The example demonstrates the scrollTo method by passing it the element id. It is important not to confuse the ordinal index and the element id. In this example, id = index + 1
!preview
Code
`tsx
const itemRendererFactory = (): VirtualListItemRenderer => ({ data, config }) => {
if (!data) {
return;
}
return {data?.name}
};
const MAX_ITEMS = 10000;
const ITEMS: IVirtualListCollection = [];
for (let i = 0, l = MAX_ITEMS; i < l; i++) {
const id = i + 1;
ITEMS.push({ id, name: Item: ${id} });
}
function App() {
const $listContainerRef = useRef(null);
const [verticalItems] = useState([...ITEMS]);
const [minId] = useState(() => {
return verticalItems1.length > 0 ? verticalItems1[0].id : 0;
});
const [maxId] = useState(() => {
return verticalItems1.length > 0 ? verticalItems1[verticalItems1.length - 1].id : 0;
});
const itemId = useRef(minId);
const onItemClick = (item: IRenderVirtualListItem) => {
console.info( Click: (ID: ${item.id}) Item ${item.data.name});
};
return <>
max={maxId} onChange={onInputScrollToIdChangeHandler} />
items={verticalItems} itemSize={40} bufferSize={50} onItemClick={onItemClick} />
>
}
`
$3
Virtual list with height-adjustable elements.
!preview
Code
`tsx
const groupItemRendererFactory = (): VirtualListItemRenderer => ({ data, config }) => {
if (!data) {
return;
}
switch (data['type']) {
case 'group-header': {
return {data?.name}
}
case 'item': {
return {data?.name}
}
}
};
const MAX_ITEMS = 10000,
CHARS = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
const generateLetter = () => {
return CHARS[Math.round(Math.random() * CHARS.length)];
}
const generateWord = () => {
const length = 5 + Math.floor(Math.random() * 50), result = [];
while (result.length < length) {
result.push(generateLetter());
}
return ${result.join('')};
};
const generateText = () => {
const length = 2 + Math.floor(Math.random() * 10), result = [];
while (result.length < length) {
result.push(generateWord());
}
let firstWord = '';
for (let i = 0, l = result[0].length; i < l; i++) {
const letter = result[0].charAt(i);
firstWord += i === 0 ? letter.toUpperCase() : letter;
}
result[0] = firstWord;
return ${result.join(' ')}.;
};
const GROUP_DYNAMIC_ITEMS: IVirtualListCollection = [],
GROUP_DYNAMIC_ITEMS_STICKY_MAP: IVirtualListStickyMap = {;
let groupDynamicIndex = 0;
for (let i = 0, l = MAX_ITEMS; i < l; i++) {
const id = i + 1, type = i === 0 || Math.random() > .895 ? 'group-header' : 'item';
if (type === 'group-header') {
groupDynamicIndex++;
}
GROUP_DYNAMIC_ITEMS.push({ id, type, name: type === 'group-header' ? Group ${id}. ${generateText()} : ${id}. ${generateText()} });
GROUP_DYNAMIC_ITEMS_STICKY_MAP[id] = type === 'group-header' ? 1 : 0;
}
function App() {
const [groupDynamicItems] = useState([...GROUP_DYNAMIC_ITEMS]);
const [groupDynamicItemsStickyMap] = useState({ ...GROUP_DYNAMIC_ITEMS_STICKY_MAP });
const onItemClick = (item: IRenderVirtualListItem) => {
console.info( Click: (ID: ${item.id}) Item ${item.data.name});
};
function App () {
return bufferSize={50} stickyMap={groupDynamicItemsStickyMap} dynamicSize={true} snap={true} onItemClick={onItemClick}/>
}
`
Stylization
List items are encapsulated in shadowDOM, so to override default styles you need to use ::part access
- Customize a scroll area of list
`css
.list .rcxvl__scroller {
scroll-behavior: auto;
/ custom scrollbar /
&::-webkit-scrollbar {
width: 16px;
height: 16px;
}
&::-webkit-scrollbar-track {
background-color: #ffffff;
}
&::-webkit-scrollbar-thumb {
background-color: #d6dee1;
border-radius: 20px;
border: 6px solid transparent;
background-clip: content-box;
min-width: 60px;
min-height: 60px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #a8bbbf;
}
}
.list {
border-radius: 3px;
box-shadow: 1px 2px 8px 4px rgba(0, 0, 0, 0.075);
border: 1px solid rgba(0, 0, 0, 0.1);
}
`
- Set up the list item canvas
`css
.list .rcxvl__list {
background-color: #ffffff;
}
`
- Set up the snapped item (Only SnappingMethod.ADVANCED)
`css
.list .rcxvl__snapped-item {
color: #71718c;
}
`
- Set up the list item
`css
.list .rcxvl__item {
background-color: unset; / override default styles /
}
`
Selecting even elements:
`tsx
const itemRendererFactory = (): VirtualListItemRenderer => (({ data, config }) => {
if (!data) {
return null;
}
let classes = 'item__content';
if (config.even) {
classes += ' even';
}
return {data?.name}
});
items={verticalItems} itemSize={40} bufferSize={50} />
`
`css
.item__content {
&.even {
background-color: #1d1d21;
}
}
``