Skip to content

VirtualList

Efficiently render large lists by only rendering visible items. Powered by @tanstack/react-virtual.

This component requires @tanstack/react-virtual as a peer dependency:

Terminal window
npm install @tanstack/react-virtual
import { VirtualList } from 'asterui/virtuallist'

Basic Usage

Efficiently render a list of 10,000 items.

import { VirtualList } from 'asterui/virtuallist'

function App() {
  const basicItems = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i + 1}`
  }))
  
  return (
      <VirtualList
        items={basicItems}
        height={300}
        itemHeight={40}
        className="border border-base-300 rounded-lg"
        renderItem={(item) => (
          <div className="p-2 border-b border-base-300 flex items-center h-full">
            {item.name}
          </div>
        )}
      />
    )
}

export default App

Styled List

With card styling and hover effects.

import { VirtualList } from 'asterui/virtuallist'

function App() {
  const users = Array.from({ length: 5000 }, (_, i) => ({
    id: i,
    name: `User ${i + 1}`,
    email: `user${i + 1}@example.com`
  }))
  
  return (
      <VirtualList
        items={users}
        height={300}
        itemHeight={60}
        className="border border-base-300 rounded-lg"
        renderItem={(user) => (
          <div className="p-3 hover:bg-base-200 transition-colors border-b border-base-300 h-full flex flex-col justify-center">
            <div className="font-medium">{user.name}</div>
            <div className="text-sm text-base-content/60">{user.email}</div>
          </div>
        )}
      />
    )
}

export default App

With Gap

Spaced items with gap between them.

import { VirtualList } from 'asterui/virtuallist'

function App() {
  const cardItems = Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    title: `Card ${i + 1}`,
    description: 'A brief description'
  }))
  
  return (
      <VirtualList
        items={cardItems}
        height={300}
        itemHeight={72}
        gap={8}
        className="p-2 border border-base-300 rounded-lg"
        renderItem={(item) => (
          <div className="card bg-base-200 p-3 h-full flex flex-col justify-center">
            <h3 className="font-bold">{item.title}</h3>
            <p className="text-sm text-base-content/70">{item.description}</p>
          </div>
        )}
      />
    )
}

export default App

Scroll Callback

Track scroll position for infinite loading or analytics.

Scroll position: 0px
import { VirtualList } from 'asterui/virtuallist'
import { useState } from 'react'

function App() {
  const scrollItems = Array.from({ length: 10000 }, (_, i) => ({ id: i }))
  const [scrollTop, setScrollTop] = useState(0)
  
  return (
      <div>
        <div className="mb-2 text-sm text-base-content/70">Scroll position: {Math.round(scrollTop)}px</div>
        <VirtualList
          items={scrollItems}
          height={250}
          itemHeight={40}
          onScroll={setScrollTop}
          className="border border-base-300 rounded-lg"
          renderItem={(_, index) => (
            <div className="p-2 border-b border-base-300 flex items-center h-full">
              Row {index + 1}
            </div>
          )}
        />
      </div>
    )
}

export default App

Variable Height (Chat)

Chat messages with different lengths using the Chat component.

import { Chat } from 'asterui'
import { VirtualList } from 'asterui/virtuallist'

function App() {
  const chatMessages = [
    { id: 1, user: 'Alex', text: 'Hey!' },
    { id: 2, user: 'You', text: 'Hi Alex, what\'s up?' },
    { id: 3, user: 'Alex', text: 'Not much, just wanted to check if you\'re coming to the party on Saturday' },
    { id: 4, user: 'You', text: 'Oh yeah, I\'ll be there! What time does it start?' },
    { id: 5, user: 'Alex', text: '8pm' },
    { id: 6, user: 'You', text: 'Cool' },
    { id: 7, user: 'Alex', text: 'Can you bring some snacks? We\'re running low on chips and stuff. Maybe grab a couple bags of tortilla chips and some salsa if you can find good ones' },
    { id: 8, user: 'You', text: 'Sure thing, I\'ll stop by the store on the way' },
    { id: 9, user: 'Alex', text: 'Thanks!' },
    { id: 10, user: 'You', text: 'No problem. Who else is coming?' },
    { id: 11, user: 'Alex', text: 'Sarah, Mike, probably Jordan. Maybe a few others' },
    { id: 12, user: 'You', text: 'Nice' },
    { id: 13, user: 'Alex', text: 'Yeah it should be fun. We\'re gonna set up the projector in the backyard for a movie later if the weather holds up' },
    { id: 14, user: 'You', text: 'What movie?' },
    { id: 15, user: 'Alex', text: 'Haven\'t decided yet. Any suggestions?' },
    { id: 16, user: 'You', text: 'How about something funny? We could do a comedy' },
    { id: 17, user: 'Alex', text: 'Good idea' },
    { id: 18, user: 'You', text: 'Alright, see you Saturday then!' },
    { id: 19, user: 'Alex', text: 'See ya!' },
    { id: 20, user: 'You', text: '👋' },
  ]
  
  return (
      <VirtualList
        items={chatMessages}
        height={300}
        itemHeight={(msg) => 70 + Math.floor(msg.text.length / 40) * 24}
        className="bg-base-100 border border-base-300 rounded-lg p-2"
        renderItem={(msg) => (
          <Chat
            position={msg.user === 'You' ? 'end' : 'start'}
            header={msg.user}
            message={msg.text}
            color={msg.user === 'You' ? 'primary' : undefined}
          />
        )}
      />
    )
}

export default App
PropertyDescriptionTypeDefault
itemsArray of items to renderT[]-
heightHeight of the scrollable containernumber | string-
itemHeightFixed height, or function returning estimated height per item for variable heightsnumber | ((item: T, index: number) => number)-
renderItemRender function for each item(item: T, index: number) => ReactNode-
overscanNumber of items to render outside visible areanumber5
gapGap between items in pixelsnumber0
widthWidth of the containernumber | string-
classNameAdditional class for the scroll containerstring-
innerClassNameAdditional class for the inner containerstring-
itemClassNameAdditional class for each item wrapperstring-
onScrollCallback when scroll position changes(scrollTop: number) => void-
data-testidTest ID for testingstring-
  • Use a fixed itemHeight when possible for best performance
  • Memoize your renderItem function if it’s expensive
  • Increase overscan if you see flickering during fast scrolling
  • Keep item components simple - avoid heavy computations in render