Svelte Virtual List
A performant Svelte 5 virtual list component that efficiently renders large datasets by only displaying visible items. Supports variable item heights, smooth scrolling with RAF optimization, and programmatic scroll-to-index functionality.
Setup #
- Svelte
<script lang="ts">
import { VirtualList } from "flowbite-svelte";
</script>Default single and multiple #
Basic virtual list displaying 5,000 items with variable text lengths. Only visible items are rendered for optimal performance.
- Svelte
<script lang="ts">
import { VirtualList } from "flowbite-svelte";
function getRandomLorem(minWords: number, maxWords: number) {
const lorem = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua".split(" ");
const wordCount = Math.floor(Math.random() * (maxWords - minWords + 1)) + minWords;
let result = [];
for (let i = 0; i < wordCount; i++) {
const word = lorem[Math.floor(Math.random() * lorem.length)];
result.push(word);
}
return result.join(" ");
}
const items = Array.from({ length: 5000 }, (_, i) => `Item ${i + 1}: ${getRandomLorem(10, 70)}`);
</script>
<VirtualList {items} minItemHeight={40} height={400} class="border p-4">
{#snippet children(item, index)}
<div class="border-b p-2 text-gray-900 dark:text-white">
{index + 1}: {item}
</div>
{/snippet}
</VirtualList>Jump to item #
Demonstrates programmatic scrolling with buttons to jump to specific items by index.
- Svelte
<script lang="ts">
import { VirtualList, Button } from "flowbite-svelte";
const items = Array.from({ length: 5000 }, (_, i) => `Item ${i + 1}`);
let scrollToFn: ((index: number) => void) | undefined;
function jumpToItem(index: number) {
scrollToFn?.(index);
}
</script>
<div class="space-y-4">
<Button onclick={() => jumpToItem(2499)}>Jump to item 2500</Button>
<Button onclick={() => jumpToItem(0)}>Jump to top item</Button>
<VirtualList {items} minItemHeight={40} height={400} scrollToIndex={(fn) => (scrollToFn = fn)}>
{#snippet children(item, index)}
<div class="border-b p-2 text-gray-900 dark:text-white" style="height:40px; line-height:40px;">
{index + 1}: {item}
</div>
{/snippet}
</VirtualList>
</div>Variable Item Heights #
Dynamically adjust item heights based on content using the getItemHeight prop.
- Svelte
<!-- VariableHeights.svelte -->
<script lang="ts">
import { VirtualList } from "flowbite-svelte";
interface Item {
title: string;
description: string;
type: "small" | "medium" | "large";
}
const items: Item[] = Array.from({ length: 1000 }, (_, i) => {
const types: Array<"small" | "medium" | "large"> = ["small", "medium", "large"];
const type = types[i % 3];
return {
title: `Item ${i + 1}`,
description:
type === "small"
? "Short description"
: type === "medium"
? "Medium length description with more details about this item"
: "Large description with lots of content. Lorem ipsum dolor sit amet, consectetur adipiscing elit. This item has much more information to display and takes up more vertical space.",
type
};
});
function getItemHeight(item: unknown): number {
const typedItem = item as Item;
return typedItem.type === "small" ? 100 : typedItem.type === "medium" ? 90 : 130;
}
</script>
<VirtualList {items} minItemHeight={100} {getItemHeight} height={400}>
{#snippet children(item, _index)}
{@const typedItem = item as Item}
<div class="border-b p-3 hover:bg-gray-50 dark:hover:bg-gray-800" style="height:{getItemHeight(typedItem)}px">
<div class="font-semibold text-gray-900 dark:text-white">{typedItem.title}</div>
<div class="mt-1 text-sm text-gray-600 dark:text-gray-400">{typedItem.description}</div>
<div class="mt-1 text-xs text-gray-500">Height: {getItemHeight(typedItem)}px</div>
</div>
{/snippet}
</VirtualList>Custom Styling #
Apply custom styles, alternating row colors, and hover effects for enhanced visual design.
- Svelte
<!-- CustomStyling.svelte -->
<script lang="ts">
import { VirtualList } from "flowbite-svelte";
interface User {
id: number;
name: string;
email: string;
status: "active" | "pending" | "inactive";
}
const items: User[] = Array.from({ length: 2000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
status: (i % 3 === 0 ? "active" : i % 3 === 1 ? "pending" : "inactive") as "active" | "pending" | "inactive"
}));
</script>
<VirtualList {items} minItemHeight={70} height={400} class="rounded-lg border">
{#snippet children(item, index)}
{@const user = item as User}
<div
class="flex items-center justify-between border-b p-4 transition-colors
{index % 2 === 0 ? 'bg-white dark:bg-gray-900' : 'bg-gray-50 dark:bg-gray-800'}
hover:bg-blue-50 dark:hover:bg-blue-900/20"
style="height:70px"
>
<div class="flex-1">
<div class="font-medium text-gray-900 dark:text-white">{user.name}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">{user.email}</div>
</div>
<span
class="rounded-full px-3 py-1 text-xs font-semibold
{user.status === 'active'
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: user.status === 'pending'
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'}"
>
{user.status}
</span>
</div>
{/snippet}
</VirtualList>Loading State #
Handle empty states and loading indicators while data is being fetched.
- Svelte
<script lang="ts">
import { VirtualList, Button, Spinner } from "flowbite-svelte";
let items: string[] = $state([]);
let isLoading = $state(false);
async function loadItems() {
isLoading = true;
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500));
items = Array.from({ length: 3000 }, (_, i) => `Item ${i + 1}`);
isLoading = false;
}
</script>
<div class="space-y-4">
<Button onclick={loadItems} disabled={isLoading}>
{#if isLoading}
<Spinner class="mr-2" size="4" />
Loading...
{:else}
Load Items
{/if}
</Button>
{#if items.length === 0 && !isLoading}
<div class="rounded-lg border p-8 text-center text-gray-500 dark:text-gray-400" style="height:400px">
<div class="mb-4 text-6xl">📋</div>
<p class="text-lg font-medium">No items yet</p>
<p class="text-sm">Click the button above to load items</p>
</div>
{:else if isLoading}
<div class="flex items-center justify-center rounded-lg border p-8" style="height:400px">
<div class="text-center">
<Spinner size="12" />
<p class="mt-4 text-gray-600 dark:text-gray-400">Loading items...</p>
</div>
</div>
{:else}
<VirtualList {items} minItemHeight={40} height={400} class="rounded-lg border">
{#snippet children(item, index)}
<div class="border-b p-2 text-gray-900 hover:bg-gray-50 dark:text-white dark:hover:bg-gray-800">
{index + 1}: {item}
</div>
{/snippet}
</VirtualList>
{/if}
</div>Interactive Items #
Add checkboxes, buttons, and other interactive elements to virtual list items.
- Svelte
<script lang="ts">
import { VirtualList, Button, Checkbox } from "flowbite-svelte";
import { TrashBinSolid } from "flowbite-svelte-icons";
interface Task {
id: number;
text: string;
completed: boolean;
}
let items = $state<Task[]>(
Array.from({ length: 2000 }, (_, i) => ({
id: i + 1,
text: `Task ${i + 1}`,
completed: false
}))
);
let selectedCount = $derived(items.filter((item) => item.completed).length);
function toggleItem(id: number) {
const item = items.find((i) => i.id === id);
if (item) item.completed = !item.completed;
}
function deleteItem(id: number) {
items = items.filter((item) => item.id !== id);
}
function clearCompleted() {
items = items.filter((item) => !item.completed);
}
</script>
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400">
{selectedCount} of {items.length} completed
</span>
{#if selectedCount > 0}
<Button size="xs" color="red" onclick={clearCompleted}>Clear Completed</Button>
{/if}
</div>
<VirtualList {items} minItemHeight={50} height={400} class="rounded-lg border">
{#snippet children(item, _index)}
{@const task = item as Task}
<div class="flex items-center gap-3 border-b p-3 hover:bg-gray-50 dark:hover:bg-gray-800" style="height:50px">
<Checkbox checked={task.completed} onchange={() => toggleItem(task.id)} />
<span class="flex-1 {task.completed ? 'text-gray-400 line-through' : 'text-gray-900 dark:text-white'}">
{task.text}
</span>
<Button size="xs" color="red" class="!p-2" onclick={() => deleteItem(task.id)}>
<TrashBinSolid class="h-3 w-3" />
</Button>
</div>
{/snippet}
</VirtualList>
</div>Large Dataset Performance #
Demonstrates smooth scrolling and rendering performance with 100,000 items.
- Svelte
<!-- LargeDataset.svelte -->
<script lang="ts">
import { VirtualList, Badge } from "flowbite-svelte";
interface Record {
id: number;
title: string;
value: number;
}
const ITEM_COUNT = 100000;
const items: Record[] = Array.from({ length: ITEM_COUNT }, (_, i) => ({
id: i + 1,
title: `Record ${i + 1}`,
value: Math.floor(Math.random() * 10000)
}));
let renderTime = $state(0);
let startTime: number;
function measureRenderStart() {
startTime = performance.now();
}
function measureRenderEnd() {
renderTime = performance.now() - startTime;
}
$effect(() => {
measureRenderStart();
return () => measureRenderEnd();
});
</script>
<div class="space-y-4">
<div class="flex items-center gap-4 text-sm">
<Badge large color="blue">
{ITEM_COUNT.toLocaleString()} items
</Badge>
{#if renderTime > 0}
<span class="text-gray-600 dark:text-gray-400">
Rendered in {renderTime.toFixed(2)}ms
</span>
{/if}
</div>
<VirtualList {items} minItemHeight={45} height={500} class="rounded-lg border">
{#snippet children(item, _index)}
{@const record = item as Record}
<div class="flex items-center justify-between border-b p-3 hover:bg-gray-50 dark:hover:bg-gray-800" style="height:45px">
<span class="text-gray-900 dark:text-white">{record.title}</span>
<span class="font-mono text-sm text-gray-600 dark:text-gray-400">
${record.value.toLocaleString()}
</span>
</div>
{/snippet}
</VirtualList>
<p class="text-xs text-gray-500 dark:text-gray-400">💡 Try scrolling through 100,000 items - notice how smooth it remains!</p>
</div>CSS containment to allow better optimization #
CSS containment tells the browser that an element’s internal layout is independent from the rest of the page, allowing better optimization and prevents layout thrashing when items are added/removed from the virtualized viewport. The browser can skip rendering work for contained elements that are off-screen.
No containment #
- Svelte
<script lang="ts">
import { VirtualList } from "flowbite-svelte";
const items = Array.from({ length: 5000 }, (_, i) => `Item ${i + 1}`);
</script>
<VirtualList {items}>
{#snippet children(item, index)}
<div>{index + 1}: {item}</div>
{/snippet}
</VirtualList>With containment #
Note: Containment may change behavior of position: sticky, overflow, and z-index stacking contexts inside items.
Enable with <VirtualList contained …> or override via classes.item.
- Svelte
<script lang="ts">
import { VirtualList } from "flowbite-svelte";
interface Article {
id: number;
thumbnail: string;
title: string;
description: string;
author: string;
date: string;
}
const items: Article[] = Array.from({ length: 5000 }, (_, i) => ({
id: i + 1,
thumbnail: `https://picsum.photos/seed/${i}/400/300`,
title: `Article ${i + 1}: ${["Tech Innovations", "Design Trends", "Web Development", "AI Insights", "Product Updates"][i % 5]}`,
description: `This is a detailed description for article ${i + 1}. It contains interesting information about the topic and provides valuable insights for readers.`,
author: ["Alice Johnson", "Bob Smith", "Carol Williams", "David Brown", "Emma Davis"][i % 5],
date: new Date(2024, 0, 1 + (i % 365)).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric"
})
}));
</script>
<VirtualList {items} contained minItemHeight={200} height={600}>
{#snippet children(item: Article, _i)}
<div class="card mb-4 rounded-lg border bg-white p-4 shadow-sm">
<img src={item.thumbnail} alt={item.title} loading="lazy" decoding="async" class="mb-3 h-48 w-full rounded-md object-cover" />
<h3 class="mb-2 text-xl font-bold">{item.title}</h3>
<p class="mb-4 text-gray-600">{item.description}</p>
<div class="metadata flex items-center justify-between text-sm text-gray-500">
<span class="font-medium">{item.author}</span>
<span>{item.date}</span>
</div>
<button class="mt-3 rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">View More</button>
</div>
{/snippet}
</VirtualList>Override via classes #
- Svelte
<script lang="ts">
import { VirtualList } from "flowbite-svelte";
const items = Array.from({ length: 5000 }, (_, i) => `Item ${i + 1}`);
</script>
<VirtualList {items} classes={{ item: "[contain:layout_style_paint] h-12" }}>
{#snippet children(item, index)}
{index + 1}: {item}
{/snippet}
</VirtualList>