Infinite Scroll
A high-performance infinite scroll component with optional virtualization that automatically loads more content as users scroll.
Usage
📱 Interactive Demo: Scroll down to see more items load automatically!
Showcase infinite scroll with product catalogs and inventory management.
Perfect for displaying posts, comments, and user-generated content.
import { InfiniteScroll } from "@/components/infinite-scroll"
import { useState, useCallback } from "react"
export default function MyComponent() {
const [items, setItems] = useState([])
const [hasNext, setHasNext] = useState(true)
const [loading, setLoading] = useState(false)
const loadMore = useCallback(async () => {
setLoading(true)
try {
const response = await fetch(`/api/items?page=${Math.floor(items.length / 10) + 1}`)
const data = await response.json()
setItems(prev => [...prev, ...data.items])
setHasNext(data.hasMore)
} catch (error) {
console.error('Failed to load items:', error)
} finally {
setLoading(false)
}
}, [items.length])
return (
<InfiniteScroll
items={items}
hasNextPage={hasNext}
isLoading={loading}
onLoadMore={loadMore}
threshold={200}
initialLoad={true}
renderItem={(item, index) => (
<div key={item.id} className="p-4 border rounded-lg">
<h3 className="font-semibold">{item.title}</h3>
<p className="text-muted-foreground">{item.description}</p>
</div>
)}
loader={() => (
<div className="flex justify-center py-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary" />
</div>
)}
endMessage={
<div className="text-center py-4 text-muted-foreground">
<p>You've reached the end! 🎉</p>
</div>
}
/>
)
}
Installation
Install the Infinite Scroll component and its dependency using your preferred package manager.
npx shadcn@latest add https://rigidui.com/r/infinite-scroll.json
Features
The Infinite Scroll component is packed with powerful features designed to provide an exceptional user experience.
Automatic Loading
Automatically loads more content when user scrolls near the bottom using Intersection Observer API.
Virtual Scrolling
Optional virtualization with TanStack Virtual for handling massive datasets with thousands of items while maintaining smooth performance.
High Performance
Uses efficient Intersection Observer instead of scroll events for better performance and battery life.
Flexible Configuration
Customizable loading triggers, custom loaders, error states, and support for reverse loading with dual rendering modes.
When to Use Virtualization
Virtualization is recommended when dealing with large datasets to maintain optimal performance.
Use Regular Mode When:
- Working with less than 1000 items
- Items have varying heights that are hard to estimate
- You need complex CSS layouts or animations
Use Virtualized Mode When:
- Displaying 1000+ items
- Items have consistent or predictable heights
- Performance is critical for your use case
- Working with data tables or feeds with many entries
Props
Prop | Type | Default |
---|---|---|
items | T[] | - |
hasNextPage | boolean | - |
isLoading | boolean | - |
onLoadMore | () => void | Promise<void> | - |
renderItem | (item: T, index: number) => React.ReactNode | - |
threshold? | number | 100 |
loader? | React.ComponentType | DefaultLoader |
endMessage? | React.ReactNode | Default end message |
errorMessage? | React.ReactNode | undefined |
className? | string | '' |
itemClassName? | string | '' |
reverse? | boolean | false |
initialLoad? | boolean | false |
scrollableTarget? | string | undefined |
virtualized? | boolean | false |
estimateSize? | () => number | () => 50 |
height? | number | 400 |
overscan? | number | 5 |
Advanced Examples
Explore advanced use cases including virtualization, performance optimization, and handling large datasets.
Virtualized Infinite Scroll
Experience high-performance scrolling with thousands of items. Only visible items are rendered to the DOM, ensuring smooth performance regardless of dataset size.
Virtual Scrolling Active
Smooth rendering with virtualization
import { InfiniteScroll } from "@/components/infinite-scroll"
import { useState, useEffect } from "react"
export default function VirtualizedExample() {
const [items, setItems] = useState([])
const [hasNext, setHasNext] = useState(true)
const [loading, setLoading] = useState(false)
const loadMore = async () => {
if (loading) return
setLoading(true)
await new Promise(resolve => setTimeout(resolve, 600))
const newItems = Array.from({ length: 20 }, (_, i) => ({
id: items.length + i + 1,
title: `Product ${items.length + i + 1}`,
description: "High-quality product with detailed specifications and reviews",
price: Math.floor(Math.random() * 500) + 50,
category: ["Electronics", "Clothing", "Home", "Sports"][Math.floor(Math.random() * 4)]
}))
setItems(prev => [...prev, ...newItems])
setHasNext(items.length + newItems.length < 500)
setLoading(false)
}
useEffect(() => {
loadMore()
}, [])
return (
<InfiniteScroll
items={items}
hasNextPage={hasNext}
isLoading={loading}
onLoadMore={loadMore}
renderItem={(item) => (
<div className="group p-6 border border-border rounded-xl bg-card hover:shadow-md transition-all duration-200">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="font-semibold text-lg text-foreground group-hover:text-primary transition-colors">
{item.title}
</h3>
<span className="text-xs bg-muted text-muted-foreground px-2 py-1 rounded-full">
{item.category}
</span>
</div>
<p className="text-muted-foreground text-sm leading-relaxed mb-3">
{item.description}
</p>
<div className="flex items-center justify-between">
<span className="text-xl font-bold text-primary">
${item.price}
</span>
<div className="flex gap-1">
{[...Array(5)].map((_, i) => (
<div key={i} className="w-3 h-3 bg-yellow-400 rounded-full text-xs"></div>
))}
</div>
</div>
</div>
</div>
</div>
)}
virtualized={true}
height={400}
estimateSize={() => 120}
overscan={3}
className="p-4"
/>
)
}