Infinite Scroll

Load more data automatically as users scroll near the end of the list. Perfect for paginated APIs, infinite feeds, and activity logs.

Interactive Demo

file · InfiniteScrollExample.svelte mode · live running open source
loaded · 50
loads · 0
range · 0-0
dom rows · 0
status · ready
Item 0 loaded page
Item 1 windowed dom
Item 2 threshold ready
Item 3 loaded page
Item 4 windowed dom
Item 5 threshold ready
Item 6 loaded page
Item 7 windowed dom
Item 8 threshold ready
Item 9 loaded page
Item 10 windowed dom
Item 11 threshold ready
Item 12 loaded page
Item 13 windowed dom
Item 14 threshold ready
Item 15 loaded page
Item 16 windowed dom
Item 17 threshold ready
Item 18 loaded page
Item 19 windowed dom
Item 20 threshold ready
threshold · 10 rows
batch · 50 rows
dom nodes · 0
cap · 500 rows
mode · append

Basic Usage

<script lang="ts">
    import VirtualList from '@humanspeak/svelte-virtual-list'

    let items = $state([...initialItems])
    let hasMore = $state(true)

    async function loadMore() {
        const newItems = await fetchMoreItems()
        items = [...items, ...newItems]

        if (newItems.length === 0) {
            hasMore = false
        }
    }
</script>

<VirtualList
    {items}
    onLoadMore={loadMore}
    loadMoreThreshold={20}
    {hasMore}
>
    {#snippet renderItem(item)}
        <div>{item.text}</div>
    {/snippet}
</VirtualList>
<script lang="ts">
    import VirtualList from '@humanspeak/svelte-virtual-list'

    let items = $state([...initialItems])
    let hasMore = $state(true)

    async function loadMore() {
        const newItems = await fetchMoreItems()
        items = [...items, ...newItems]

        if (newItems.length === 0) {
            hasMore = false
        }
    }
</script>

<VirtualList
    {items}
    onLoadMore={loadMore}
    loadMoreThreshold={20}
    {hasMore}
>
    {#snippet renderItem(item)}
        <div>{item.text}</div>
    {/snippet}
</VirtualList>

Props

PropTypeDefaultDescription
onLoadMore() => void \| Promise<void>-Callback when more data is needed (supports async)
loadMoreThresholdnumber20Number of items from the end to trigger onLoadMore
hasMorebooleantrueSet to false when all data has been loaded

Behavior

  • Triggers when scrolling near the end - The callback fires when the user scrolls within loadMoreThreshold items of the end.
  • Automatic initial trigger - If the initial items are below the threshold, onLoadMore is called automatically on mount.
  • Prevents concurrent calls - While onLoadMore is running (for async functions), additional calls are prevented.

Complete Example

Here’s a more complete example with loading states and error handling:

<script lang="ts">
    import VirtualList from '@humanspeak/svelte-virtual-list'

    type Item = { id: number; text: string }

    let items = $state<Item[]>([])
    let hasMore = $state(true)
    let isLoading = $state(false)
    let error = $state<string | null>(null)
    let page = $state(1)

    // Initial load
    $effect(() => {
        loadInitial()
    })

    async function loadInitial() {
        isLoading = true
        try {
            const response = await fetch('/api/items?page=1')
            const data = await response.json()
            items = data.items
            hasMore = data.hasMore
            page = 1
        } catch (e) {
            error = 'Failed to load items'
        } finally {
            isLoading = false
        }
    }

    async function loadMore() {
        if (isLoading) return

        isLoading = true
        error = null

        try {
            const nextPage = page + 1
            const response = await fetch(`/api/items?page=${nextPage}`)
            const data = await response.json()

            items = [...items, ...data.items]
            hasMore = data.hasMore
            page = nextPage
        } catch (e) {
            error = 'Failed to load more items'
        } finally {
            isLoading = false
        }
    }
</script>

{#if error}
    <div class="error">
        {error}
        <button onclick={loadMore}>Retry</button>
    </div>
{/if}

<VirtualList
    {items}
    onLoadMore={loadMore}
    loadMoreThreshold={10}
    {hasMore}
>
    {#snippet renderItem(item)}
        <div class="item">{item.text}</div>
    {/snippet}
</VirtualList>

{#if isLoading}
    <div class="loading">Loading...</div>
{/if}
<script lang="ts">
    import VirtualList from '@humanspeak/svelte-virtual-list'

    type Item = { id: number; text: string }

    let items = $state<Item[]>([])
    let hasMore = $state(true)
    let isLoading = $state(false)
    let error = $state<string | null>(null)
    let page = $state(1)

    // Initial load
    $effect(() => {
        loadInitial()
    })

    async function loadInitial() {
        isLoading = true
        try {
            const response = await fetch('/api/items?page=1')
            const data = await response.json()
            items = data.items
            hasMore = data.hasMore
            page = 1
        } catch (e) {
            error = 'Failed to load items'
        } finally {
            isLoading = false
        }
    }

    async function loadMore() {
        if (isLoading) return

        isLoading = true
        error = null

        try {
            const nextPage = page + 1
            const response = await fetch(`/api/items?page=${nextPage}`)
            const data = await response.json()

            items = [...items, ...data.items]
            hasMore = data.hasMore
            page = nextPage
        } catch (e) {
            error = 'Failed to load more items'
        } finally {
            isLoading = false
        }
    }
</script>

{#if error}
    <div class="error">
        {error}
        <button onclick={loadMore}>Retry</button>
    </div>
{/if}

<VirtualList
    {items}
    onLoadMore={loadMore}
    loadMoreThreshold={10}
    {hasMore}
>
    {#snippet renderItem(item)}
        <div class="item">{item.text}</div>
    {/snippet}
</VirtualList>

{#if isLoading}
    <div class="loading">Loading...</div>
{/if}

Cursor-Based Pagination

For APIs that use cursor-based pagination:

<script lang="ts">
    import VirtualList from '@humanspeak/svelte-virtual-list'

    let items = $state<Item[]>([])
    let cursor = $state<string | null>(null)
    let hasMore = $state(true)

    async function loadMore() {
        const params = new URLSearchParams({ limit: '50' })
        if (cursor) {
            params.set('cursor', cursor)
        }

        const response = await fetch(`/api/items?${params}`)
        const data = await response.json()

        items = [...items, ...data.items]
        cursor = data.nextCursor
        hasMore = data.hasMore
    }
</script>

<VirtualList {items} onLoadMore={loadMore} {hasMore}>
    {#snippet renderItem(item)}
        <div>{item.text}</div>
    {/snippet}
</VirtualList>
<script lang="ts">
    import VirtualList from '@humanspeak/svelte-virtual-list'

    let items = $state<Item[]>([])
    let cursor = $state<string | null>(null)
    let hasMore = $state(true)

    async function loadMore() {
        const params = new URLSearchParams({ limit: '50' })
        if (cursor) {
            params.set('cursor', cursor)
        }

        const response = await fetch(`/api/items?${params}`)
        const data = await response.json()

        items = [...items, ...data.items]
        cursor = data.nextCursor
        hasMore = data.hasMore
    }
</script>

<VirtualList {items} onLoadMore={loadMore} {hasMore}>
    {#snippet renderItem(item)}
        <div>{item.text}</div>
    {/snippet}
</VirtualList>

Integration Guides

For real-world integrations with backend services: