core
Basic List
Svelte implementation — the core of @floor/vlist. Use
the control panel to explore item count, overscan, scroll-to, and
data operations in real time.
Source
// Basic List — Svelte implementation with vlist action
// Interactive control panel demonstrating core vlist API: item count, overscan,
// scrollToIndex, and data operations (append, prepend, remove).
import { vlist } from "vlist-svelte";
import {
DEFAULT_COUNT,
ITEM_HEIGHT,
makeUser,
makeUsers,
itemTemplate,
} from "../shared.js";
// =============================================================================
// DOM References
// =============================================================================
const container = document.getElementById("list-container");
// Sliders
const countSlider = document.getElementById("count-slider");
const countValue = document.getElementById("count-value");
const overscanSlider = document.getElementById("overscan-slider");
const overscanValue = document.getElementById("overscan-value");
// Scroll To
const scrollIndexInput = document.getElementById("scroll-index");
const scrollAlignSelect = document.getElementById("scroll-align");
const scrollGoBtn = document.getElementById("scroll-go");
const btnFirst = document.getElementById("btn-first");
const btnMiddle = document.getElementById("btn-middle");
const btnLast = document.getElementById("btn-last");
// Data Operations
const btnPrepend = document.getElementById("btn-prepend");
const btnAppend = document.getElementById("btn-append");
const btnAppend100 = document.getElementById("btn-append-100");
const btnRemove = document.getElementById("btn-remove");
const btnClear = document.getElementById("btn-clear");
const btnReset = document.getElementById("btn-reset");
// Footer
const ftProgress = document.getElementById("ft-progress");
const ftDom = document.getElementById("ft-dom");
const ftTotal = document.getElementById("ft-total");
const ftHeight = document.getElementById("ft-height");
const ftOverscan = document.getElementById("ft-overscan");
// =============================================================================
// State
// =============================================================================
let users = makeUsers(DEFAULT_COUNT);
let nextId = DEFAULT_COUNT + 1;
let currentOverscan = 3;
let action = null;
let listInstance = null;
let selectedIndex = -1;
// =============================================================================
// Create / Recreate list
// =============================================================================
function createList() {
// Destroy previous action
if (action && action.destroy) {
action.destroy();
action = null;
listInstance = null;
}
// Clear container
container.innerHTML = "";
// Create vlist action
action = vlist(container, {
config: {
ariaLabel: "User list",
overscan: currentOverscan,
items: users,
item: {
height: ITEM_HEIGHT,
template: itemTemplate,
},
selection: { mode: 'single' },
},
onInstance: (inst) => {
listInstance = inst;
selectedIndex = -1;
bindListEvents();
updateFooter();
},
});
}
// =============================================================================
// Event bindings
// =============================================================================
function bindListEvents() {
if (!listInstance) return;
listInstance.on('selection:change', ({ selected }) => {
selectedIndex = selected.length > 0 ? selected[0] : -1;
});
listInstance.on("range:change", () => {
updateFooter();
});
listInstance.on("scroll", () => {
updateFooter();
});
}
// =============================================================================
// Footer
// =============================================================================
function updateFooter() {
const domNodes = container.querySelectorAll(".vlist-item").length;
const total = users.length;
const progress = total > 0 ? Math.round((domNodes / total) * 100) : 0;
ftProgress.textContent = `${progress}%`;
ftDom.textContent = domNodes;
ftTotal.textContent = total.toLocaleString();
ftHeight.textContent = ITEM_HEIGHT;
ftOverscan.textContent = currentOverscan;
}
// =============================================================================
// Item Count Slider
// =============================================================================
countSlider.addEventListener("input", () => {
const count = parseInt(countSlider.value, 10);
countValue.textContent = count.toLocaleString();
});
countSlider.addEventListener("change", () => {
const count = parseInt(countSlider.value, 10);
users = makeUsers(count);
nextId = count + 1;
createList();
});
// =============================================================================
// Overscan Slider
// =============================================================================
overscanSlider.addEventListener("input", () => {
overscanValue.textContent = overscanSlider.value;
});
overscanSlider.addEventListener("change", () => {
currentOverscan = parseInt(overscanSlider.value, 10);
createList();
});
// =============================================================================
// Scroll To
// =============================================================================
const doScrollTo = () => {
if (!listInstance) return;
const index = parseInt(scrollIndexInput.value, 10);
const align = scrollAlignSelect.value;
if (isNaN(index) || index < 0) return;
listInstance.scrollToIndex(Math.min(index, users.length - 1), {
align,
behavior: "smooth",
duration: 400,
});
};
scrollGoBtn.addEventListener("click", doScrollTo);
scrollIndexInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") doScrollTo();
});
btnFirst.addEventListener("click", () => {
listInstance?.scrollToIndex(0, { behavior: "smooth", duration: 300 });
});
btnMiddle.addEventListener("click", () => {
listInstance?.scrollToIndex(Math.floor(users.length / 2), {
align: "center",
behavior: "smooth",
duration: 500,
});
});
btnLast.addEventListener("click", () => {
listInstance?.scrollToIndex(users.length - 1, {
align: "end",
behavior: "smooth",
duration: 500,
});
});
// =============================================================================
// Data Operations
// =============================================================================
function syncCountSlider() {
const clamped = Math.min(users.length, parseInt(countSlider.max, 10));
countSlider.value = clamped;
countValue.textContent = users.length.toLocaleString();
}
btnAppend.addEventListener("click", () => {
const newUser = makeUser(nextId);
nextId++;
users = [...users, newUser];
listInstance?.appendItems([newUser]);
syncCountSlider();
updateFooter();
});
btnPrepend.addEventListener("click", () => {
const newUser = makeUser(nextId);
nextId++;
users = [newUser, ...users];
listInstance?.prependItems([newUser]);
syncCountSlider();
updateFooter();
});
btnAppend100.addEventListener("click", () => {
const batch = makeUsers(100, nextId);
nextId += 100;
users = [...users, ...batch];
listInstance?.appendItems(batch);
syncCountSlider();
updateFooter();
});
btnRemove.addEventListener("click", () => {
if (users.length === 0) return;
const idx = selectedIndex >= 0 && selectedIndex < users.length
? selectedIndex
: users.length - 1;
users = users.filter((_, i) => i !== idx);
listInstance?.clearSelection();
selectedIndex = -1;
listInstance?.setItems(users);
syncCountSlider();
updateFooter();
});
btnClear.addEventListener("click", () => {
users = [];
listInstance?.setItems(users);
syncCountSlider();
updateFooter();
});
btnReset.addEventListener("click", () => {
users = makeUsers(DEFAULT_COUNT);
nextId = DEFAULT_COUNT + 1;
currentOverscan = 3;
countSlider.value = DEFAULT_COUNT;
countValue.textContent = DEFAULT_COUNT.toLocaleString();
overscanSlider.value = 3;
overscanValue.textContent = "3";
createList();
});
// =============================================================================
// Initialise
// =============================================================================
createList();
<div class="container">
<header>
<h1>Basic List</h1>
<p class="description">
Svelte implementation — the core of <code>@floor/vlist</code>. Use
the control panel to explore item count, overscan, scroll-to, and
data operations in real time.
</p>
</header>
<div class="split-layout">
<div class="split-main">
<div id="list-container"></div>
</div>
<aside class="split-panel">
<!-- Items -->
<section class="panel-section">
<h3 class="panel-title">Items</h3>
<div class="panel-row">
<label class="panel-label">Count</label>
<span class="panel-value" id="count-value">10,000</span>
</div>
<div class="panel-row">
<input
type="range"
id="count-slider"
class="panel-slider"
min="100"
max="100000"
step="100"
value="10000"
/>
</div>
<div class="panel-row">
<label class="panel-label">Overscan</label>
<span class="panel-value" id="overscan-value">3</span>
</div>
<div class="panel-row">
<input
type="range"
id="overscan-slider"
class="panel-slider"
min="0"
max="10"
step="1"
value="3"
/>
</div>
</section>
<!-- Scroll To -->
<section class="panel-section">
<h3 class="panel-title">Scroll To</h3>
<div class="panel-row">
<div class="panel-input-group">
<input
type="number"
id="scroll-index"
class="panel-input"
placeholder="Index"
min="0"
value="0"
/>
<select id="scroll-align" class="panel-select">
<option value="start">start</option>
<option value="center">center</option>
<option value="end">end</option>
</select>
<button id="scroll-go" class="panel-btn">Go</button>
</div>
</div>
<div class="panel-row">
<div class="panel-btn-group">
<button
id="btn-first"
class="panel-btn panel-btn--icon"
title="First"
>
<i class="icon icon--up"></i>
</button>
<button
id="btn-middle"
class="panel-btn panel-btn--icon"
title="Middle"
>
<i class="icon icon--center"></i>
</button>
<button
id="btn-last"
class="panel-btn panel-btn--icon"
title="Last"
>
<i class="icon icon--down"></i>
</button>
</div>
</div>
</section>
<!-- Data Operations -->
<section class="panel-section">
<h3 class="panel-title">Data</h3>
<div class="panel-row">
<div class="panel-btn-group">
<button
id="btn-prepend"
class="panel-btn"
title="Prepend 1 item"
>
<i class="icon icon--add"></i> Prepend
</button>
<button
id="btn-append"
class="panel-btn"
title="Append 1 item"
>
<i class="icon icon--add"></i> Append
</button>
<button
id="btn-append-100"
class="panel-btn"
title="Append 100 items"
>
<i class="icon icon--add"></i> +100
</button>
</div>
</div>
<div class="panel-row">
<div class="panel-btn-group">
<button
id="btn-remove"
class="panel-btn"
title="Remove last item"
>
<i class="icon icon--remove"></i> Remove
</button>
<button
id="btn-clear"
class="panel-btn"
title="Clear all items"
>
<i class="icon icon--trash"></i> Clear
</button>
<button
id="btn-reset"
class="panel-btn"
title="Reset to 10,000 items"
>
<i class="icon icon--shuffle"></i> Reset
</button>
</div>
</div>
</section>
</aside>
</div>
<footer class="example-footer" id="example-footer">
<div class="example-footer__left">
<span class="example-footer__stat">
<strong id="ft-progress">0%</strong>
</span>
<span class="example-footer__stat">
<span id="ft-velocity">0.00</span> /
<strong id="ft-velocity-avg">0.00</strong>
<span class="example-footer__unit">px/ms</span>
</span>
<span class="example-footer__stat">
<span id="ft-dom">0</span> /
<strong id="ft-total">0</strong>
<span class="example-footer__unit">items</span>
</span>
</div>
<div class="example-footer__right">
<span class="example-footer__stat">
height <strong id="ft-height">56</strong
><span class="example-footer__unit">px</span>
</span>
<span class="example-footer__stat">
overscan <strong id="ft-overscan">3</strong>
</span>
</div>
</footer>
</div>
// Shared data and utilities for basic list example variants
// This file is imported by all framework implementations to avoid duplication
import { makeUser, makeUsers } from '../../src/data/people.js';
// =============================================================================
// Constants
// =============================================================================
export const DEFAULT_COUNT = 10_000;
export const ITEM_HEIGHT = 56;
// =============================================================================
// Data Generation (re-export for convenience)
// =============================================================================
export { makeUser, makeUsers };
// =============================================================================
// Templates
// =============================================================================
export const itemTemplate = (user, i) => `
<div class="item__avatar" style="background:${user.color}">${user.initials}</div>
<div class="item__text">
<div class="item__name">${user.name}</div>
<div class="item__email">${user.email}</div>
</div>
<span class="item__index">#${i + 1}</span>
`;
/* Basic List — example styles */
/* ============================================================================
Item (styles live on .vlist-item — no wrapper div needed)
============================================================================ */
.vlist-item {
display: flex;
align-items: center;
gap: 12px;
padding: 0 16px;
}
.item__avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 15px;
flex-shrink: 0;
}
.item__text {
flex: 1;
min-width: 0;
}
.item__name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item__email {
font-size: 13px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item__index {
font-size: 12px;
color: var(--text-muted);
min-width: 48px;
text-align: right;
font-variant-numeric: tabular-nums;
}
/* ============================================================================
Selected state
============================================================================ */