Skip to content

About

πŸ“œ A performant virtual list/scrolling component for Svelte applications - efficiently render large scrollable lists with minimal memory usage

Topics

Resources

Stars

108 stars

Watchers

1 watching

Forks

Latest commit

Β 

History

776 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

@humanspeak/svelte-virtual-list

NPM version AI tokens used building this repo β€” TokenMaxing Build Status Coverage Status License Downloads CodeQL Install size Code Style: Trunk TypeScript Types Maintenance

A high-performance virtual list component for Svelte 5 applications that efficiently renders large datasets with minimal memory usage.

Features

  • πŸ“ Dynamic item height handling - no fixed height required
  • πŸ”„ Automatic resize handling for dynamic content
  • πŸ“ TypeScript support with full type safety
  • πŸš€ SSR compatible with hydration support
  • ✨ Svelte 5 runes and snippets support
  • 🎨 Customizable styling with class props
  • πŸ› Debug mode for development
  • 🎯 Smooth scrolling with configurable buffer zones
  • 🧠 Memory-optimized for 10k+ items
  • πŸ§ͺ Comprehensive test coverage (vitest and playwright)
  • πŸš€ Progressive initialization for large datasets
  • πŸ•ΉοΈ Programmatic scrolling with scroll
  • ♾️ Infinite scroll support with onLoadMore
  • β™Ώ Keyboard-accessible viewport (focusable labeled region, standard scroll keys)

Requirements

  • Svelte 5
  • Node.js 18+

Installation

# Using pnpm (recommended)
pnpm add @humanspeak/svelte-virtual-list

# Using npm
npm install @humanspeak/svelte-virtual-list

# Using yarn
yarn add @humanspeak/svelte-virtual-list

Basic Usage

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

    const items = Array.from({ length: 1000 }, (_, i) => ({
        id: i,
        text: `Item ${i}`
    }))
</script>

<SvelteVirtualList {items}>
    {#snippet renderItem(item)}
        <div>{item.text}</div>
    {/snippet}
</SvelteVirtualList>

Props

Prop Type Default Description
items T[] Required Array of items to render
itemKey (item, index) => string | number - Stable identity for measurement and responsive anchor preservation
orientation 'vertical' | 'horizontal' 'vertical' Active layout axis; may change reactively (horizontal is LTR-only)
defaultEstimatedItemSize number 40 Axis-neutral initial size; takes precedence over the height alias
defaultEstimatedItemHeight number 40 Initial height estimate used until items are measured
bufferSize number 20 Number of items rendered outside the viewport
debug boolean false Enable debug logging and visualizations
containerClass string '' Class for outer container
viewportClass string '' Class for scrollable viewport
viewportLabel string 'Scrollable list' Accessible label for the focusable viewport region
contentClass string '' Class for content wrapper
itemsClass string '' Class for items container
testId string '' Base test id used in internal test hooks (useful for E2E/tests and debugging)
onLoadMore () => void | Promise<void> - Callback when more data is needed for infinite scroll
loadMoreThreshold number 20 Items from end to trigger onLoadMore
hasMore boolean true Set to false when all data has been loaded
onRangeChange (range) => void - Fires when the rendered range or at-top/at-bottom state changes

Measured header and footer

Optional header?: Snippet and footer?: Snippet take no arguments. They stay mounted outside the virtualized rows and scroll with content; they are not sticky. In horizontal mode they are leading and trailing content. Their wrappers isolate child margins and add no spacing; snippets provide their own visual dimensions.

<SvelteVirtualList {items}>
    {#snippet header()}<h2>Results</h2>{/snippet}
    {#snippet renderItem(item)}<article>{item.title}</article>{/snippet}
    {#snippet footer()}<p>End of results</p>{/snippet}
</SvelteVirtualList>

Content extent is measured header + estimated/measured rows + measured footer. Short lists stay at the start, with spare viewport space after the footer. Row counts, indices, averages and loading thresholds exclude snippets. Header/footer resize preserves the visible reading row, physical start stays at zero, and a pinned physical end stays pinned. Replacement, removal and orientation changes remeasure the content.

Index alignment includes the header offset. Aligning the last row to end leaves the footer below that row; scrollToOffset addresses the entire content from zero, and the End key reaches its physical end. Both onRangeChange.atBottom and debug atBottom include the footer. Debug totalHeight reports the full content extent on the active axis; averageItemHeight and counts remain row-only.

Observing the visible range

onRangeChange delivers a { start, end, atTop, atBottom } snapshot whenever the rendered item range or the scroll edges change (it also fires once after mount with the initial range). Use it for impression/analytics tracking, URL sync, sticky toolbars, or scroll-position persistence β€” no need to enable debug. start/end include the render buffer.

<SvelteVirtualList
    {items}
    onRangeChange={({ start, end, atTop, atBottom }) => {
        console.log(`rendering ${start}..${end}`, { atTop, atBottom })
    }}
>
    {#snippet renderItem(item)}
        <div>{item.text}</div>
    {/snippet}
</SvelteVirtualList>

Programmatic Scrolling

Scroll to any item in the list using the scroll method. Useful for jump-to-item navigation, search results, and more.

<script lang="ts">
    import SvelteVirtualList from '@humanspeak/svelte-virtual-list'
    let listRef
    const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }))

    function goToItem5000() {
        listRef.scroll({ index: 5000, smoothScroll: true, align: 'auto' })
    }

    function restoreScrollPosition() {
        listRef.scrollToOffset({ offset: 12345, smoothScroll: false })
    }
</script>

<button onclick={goToItem5000}> Scroll to item 5000 </button>
<button onclick={restoreScrollPosition}> Restore scroll position </button>
<SvelteVirtualList {items} bind:this={listRef}>
    {#snippet renderItem(item)}
        <div>{item.text}</div>
    {/snippet}
</SvelteVirtualList>

scroll() Options

Option Type Default Description
index number Required The item index to scroll to (0-based)
smoothScroll boolean true Use smooth scrolling animation
shouldThrowOnBounds boolean true Throw if index is out of bounds
align 'auto' | 'top' | 'bottom' | 'nearest' | 'center' 'auto' Where to align the item in the viewport

Alignment options:

  • 'auto' - Only scroll if not visible, align to nearest edge
  • 'top' - Always align to the top
  • 'bottom' - Always align to the bottom
  • 'nearest' - Scroll as little as possible to bring the item into view
  • 'center' - Center the item in the viewport, clamped at the list edges

scrollToOffset() Options

Use scrollToOffset({ offset, smoothScroll }) to scroll to a raw pixel offset, such as restoring a persisted scroll position after navigation. The returned promise resolves after scrolling has visually finished.

Option Type Default Description
offset number Required Raw offset in pixels along the active axis
smoothScroll boolean true Use smooth scrolling animation

Horizontal and responsive orientation

Use orientation="horizontal" for an LTR horizontal list. The prop is reactive, so a keyed list can switch axes at a breakpoint while retaining the same logical visible item. Measurements are axis-specific and are cleared during the switch.

<SvelteVirtualList
    {items}
    itemKey={(item) => item.id}
    orientation={mobile ? 'horizontal' : 'vertical'}
    defaultEstimatedItemSize={120}
>
    {#snippet renderItem(item)}<article>{item.title}</article>{/snippet}
</SvelteVirtualList>

start/end, nearest, center, scrollToOffset(), range callbacks, and infinite loading follow the active axis. In horizontal mode Left/Right scroll by one line; Home/End go to the logical edges; Page Up/Page Down and Space page the viewport. Up/Down and keys inside interactive children are left alone. RTL horizontal behavior is not supported in this release.

See the horizontal guide or open the interactive 10,000-item example to switch between horizontal, vertical, and responsive modes.

Infinite Scroll

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

<script lang="ts">
    import SvelteVirtualList 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>

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

Infinite Scroll Behavior

  • Triggers when scrolling near the end of the list
  • Automatically triggers on mount if initial items are below threshold
  • Prevents concurrent onLoadMore calls while loading
  • Works with both sync and async callbacks

Integration Guides

Performance Considerations

  • The bufferSize prop affects memory usage and scroll smoothness
  • Items are measured and cached for optimal performance
  • Dynamic height calculations happen automatically
  • Resize observers handle container/content changes
  • Virtual DOM updates are batched for efficiency

Testing

Unit Tests (Vitest)

# Run unit tests with coverage
pnpm test

# Run specific test files
pnpm vitest src/lib/utils/throttle.test.ts

E2E Tests (Playwright)

# Install Playwright browsers (one-time setup)
npx playwright install

# Run all e2e tests
pnpm run test:e2e

# Run specific e2e test
npx playwright test tests/docs-visit.spec.ts --project=chromium

# Debug mode
npx playwright test --debug

Project Structure

This is a PNPM workspace with two packages:

  1. ./ - Main Svelte Virtual List component package
  2. ./docs - Documentation site with live demos and examples

Development Commands

# Install dependencies for both packages
pnpm install

# Start development server
pnpm dev

# Start both package and docs
pnpm run dev:all

# Build package
pnpm run build

# Check TypeScript/Svelte
pnpm run check

# Format and lint code (uses Trunk)
trunk fmt
trunk check

# Run all tests
pnpm test:all

This project uses Trunk for formatting and linting. Trunk manages tool versions and runs checks automatically via pre-commit hooks.

Svelte 5 ecosystem

Part of the Humanspeak family of runes-native Svelte 5 packages:

Package Description
@humanspeak/svelte-markdown Runtime markdown renderer for Svelte
@humanspeak/svelte-virtual-list β€” this package Virtual scrolling for Svelte
@humanspeak/svelte-motion Framer Motion for Svelte 5
@humanspeak/svelte-headless-table Headless data tables for Svelte
@humanspeak/svelte-virtual-chat Virtual chat viewport for Svelte 5
@humanspeak/svelte-diff Diff comparison for Svelte
@humanspeak/svelte-purify HTML sanitisation for Svelte
@humanspeak/memory-cache In-memory cache for TypeScript
@humanspeak/svelte-json-view-lite JSON tree viewer for Svelte 5
@humanspeak/svelte-scoped-props Scoped class props for Svelte

License

MIT Β© Humanspeak, Inc.

Credits

Made with ❀️ by Humanspeak

About

πŸ“œ A performant virtual list/scrolling component for Svelte applications - efficiently render large scrollable lists with minimal memory usage

Topics

Resources

Stars

108 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages