A high-performance infinite scrolling component that virtualizes large lists by only rendering visible items. Built with Lit and optimized for smooth scrolling with thousands of items.
- 🚀 Virtual Scrolling: Only renders items that are visible in the viewport
- ⚡ High Performance: Handles thousands of items with minimal memory usage
- 🔄 Dynamic Buffering: Automatically calculates optimal buffer sizes based on viewport
- 📱 Responsive: Adapts to container size changes automatically
- 🎯 Smooth Scrolling: Uses CSS transforms and optimized rendering for 60fps performance
- 🛠️ Abstract Base Class: Extend and customize for your specific use cases
- 🎨 Styleable: Full CSS customization with CSS custom properties
npm install @arcmantle/infinite-scroller
pnpm add @arcmantle/infinite-scroller
yarn add @arcmantle/infinite-scroller
The InfiniteScroller
is an abstract base class that you extend to create your own infinite scrolling components:
import { InfiniteScroller } from '@arcmantle/infinite-scroller';
import { customElement } from 'lit/decorators.js';
@customElement('my-list')
export class MyListComponent extends InfiniteScroller {
constructor() {
super();
this.maxIndex = 10000; // Total number of items
}
// Create the DOM element for each list item
protected createElement(): HTMLElement {
return document.createElement('my-list-item');
}
// Update the element content based on its index
protected updateElement(element: HTMLElement, index: number): void {
if (index < 0 || index >= this.maxIndex) {
element.style.visibility = 'hidden';
return;
}
element.style.visibility = 'visible';
element.textContent = `Item ${index}`;
}
}
Here's a more complete example with custom styling and data:
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { InfiniteScroller } from '@arcmantle/infinite-scroller';
interface ListItem {
id: string;
title: string;
description: string;
}
@customElement('data-list')
export class DataListComponent extends InfiniteScroller {
@property({ type: Array })
data: ListItem[] = [];
constructor() {
super();
this.maxIndex = this.data.length;
}
protected createElement(): HTMLElement {
return document.createElement('data-list-item');
}
protected updateElement(element: DataListItemComponent, index: number): void {
if (index < 0 || index >= this.data.length) {
element.style.visibility = 'hidden';
return;
}
element.style.visibility = 'visible';
element.item = this.data[index];
}
// Handle infinite loading
protected override onScroll(): void {
super.onScroll();
// Load more data when near the end
if ((this.maxIndex - this.position) < 30) {
this.loadMoreData();
}
}
private async loadMoreData(): Promise<void> {
const newData = await fetchMoreItems();
this.data = [...this.data, ...newData];
this.maxIndex = this.data.length;
}
static override styles = css`
:host {
--item-height: 80px;
height: 400px;
border: 1px solid #ccc;
}
`;
}
@customElement('data-list-item')
export class DataListItemComponent extends LitElement {
@property({ type: Object })
item?: ListItem;
protected render() {
if (!this.item) return html``;
return html`
<div class="item">
<h3>${this.item.title}</h3>
<p>${this.item.description}</p>
</div>
`;
}
static styles = css`
:host {
display: block;
padding: 16px;
border-bottom: 1px solid #eee;
}
.item h3 {
margin: 0 0 8px 0;
font-size: 16px;
}
.item p {
margin: 0;
color: #666;
font-size: 14px;
}
`;
}
Property | Type | Description |
---|---|---|
maxIndex |
number |
Total number of items in the list |
itemHeight |
number |
Height of each item in pixels |
bufferSize |
number |
Number of items to render outside viewport (calculated automatically) |
position |
number |
Current scroll position as item index (can be fractional) |
Property | Default | Description |
---|---|---|
--item-height |
60px |
Height of each list item |
// Create a new DOM element for list items
protected abstract createElement(): HTMLElement;
// Update element content based on its index position
protected abstract updateElement(element: HTMLElement, index: number): void;
// Get/set scroll position by item index
scroller.position = 100; // Scroll to item 100
const currentPosition = scroller.position;
// Override to handle scroll events
protected onScroll(): void {
super.onScroll();
// Your custom scroll logic
}
// Override to handle resize events
protected onResize(entries?: ResizeObserverEntry[]): boolean {
const result = super.onResize(entries);
// Your custom resize logic
return result;
}
The infinite scroller uses a dual-buffer strategy:
- Two Buffers: Maintains two buffer zones that contain rendered items
- Dynamic Translation: Buffers are translated vertically as the user scrolls
- Automatic Sizing: Buffer size is calculated based on viewport height
- Smart Updates: Only updates items that are actually visible
┌─────────────────┐
│ Buffer 0 │ ← Contains items 0-19
├─────────────────┤
│ Buffer 1 │ ← Contains items 20-39
├─────────────────┤
│ (Virtual) │ ← Items 40+ not rendered
└─────────────────┘
As the user scrolls down:
- Buffer 0 moves below Buffer 1 and updates to show items 40-59
- Buffer 1 continues showing items 20-39
- The cycle continues seamlessly
- CSS Transforms: Uses
translate3d()
for hardware acceleration - Passive Scrolling: Scroll listeners are passive for better performance
- ResizeObserver: Efficiently handles container size changes
- Minimal DOM: Only creates elements that fit in the buffers
- Smart Updates: Only updates visible elements during scroll
// Fired when the scroller is ready and initialized
scroller.addEventListener('ready', (event) => {
console.log('Scroller is ready');
});
The component supports all standard scroll events on the internal scroller element.
my-list {
--item-height: 100px;
height: 500px;
width: 100%;
border: 1px solid #ddd;
}
my-list::part(scroller) {
border-radius: 8px;
background: #f9f9f9;
}
my-list::part(buffer) {
/* Style the buffer containers */
}
my-list {
--item-height: 60px;
height: 100%;
}
@media (max-width: 768px) {
my-list {
--item-height: 80px;
}
}
protected override onScroll(): void {
super.onScroll();
const threshold = 50; // Items from end
if ((this.maxIndex - this.position) < threshold) {
this.loadMoreItems();
}
}
private async loadMoreItems(): Promise<void> {
if (this.loading) return;
this.loading = true;
const newItems = await this.dataService.fetchMore();
this.data.push(...newItems);
this.maxIndex = this.data.length;
this.loading = false;
}
private filteredData: Item[] = [];
search(query: string): void {
this.filteredData = this.allData.filter(item =>
item.title.toLowerCase().includes(query.toLowerCase())
);
this.maxIndex = this.filteredData.length;
this.position = 0; // Reset to top
}
protected updateElement(element: HTMLElement, index: number): void {
const item = this.filteredData[index];
// Update element with filtered data
}
For variable item heights, calculate and cache heights:
private itemHeights = new Map<number, number>();
protected override get itemHeight(): number {
// Return average height or base height
return this.averageItemHeight || 60;
}
protected updateElement(element: HTMLElement, index: number): void {
super.updateElement(element, index);
// Measure and cache actual height
requestAnimationFrame(() => {
const height = element.getBoundingClientRect().height;
this.itemHeights.set(index, height);
});
}
- Modern Browsers: Chrome 69+, Firefox 63+, Safari 12+
- Required Features:
- ResizeObserver API
- CSS Grid Layout
- CSS Custom Properties
- ES2020+ JavaScript features
- Keep Updates Light: Minimize work in
updateElement()
- Use CSS for Styling: Prefer CSS over JavaScript for visual changes
- Batch DOM Updates: Group multiple changes together
- Profile Your Code: Use browser dev tools to identify bottlenecks
- Consider Item Complexity: Simpler items = better performance
Items not updating correctly:
- Ensure
updateElement()
handles all edge cases - Check that
maxIndex
is set correctly
Scrolling feels sluggish:
- Reduce complexity in
updateElement()
- Check for memory leaks in item components
Layout jumping:
- Ensure consistent
--item-height
values - Avoid dynamic height changes during scroll
Buffer size warnings:
- Increase container height or decrease item height
- Check for CSS issues affecting measurements
pnpm install
pnpm build
pnpm dev
pnpm test
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project is licensed under the Apache 2.0 License - see the Apache License 2.0 for details.
This component is part of the @arcmantle ecosystem:
@arcmantle/library
- Core utilities and helper functionslit
- The underlying web component framework
.