Rigid UI

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!

Product Listing Feature

Showcase infinite scroll with product catalogs and inventory management.

Social Media Feed

Perfect for displaying posts, comments, and user-generated content.

infinite-scroll.tsx
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

PropTypeDefault
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

0/500 loaded
Only visible items rendered
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"
    />
  )
}