/ Examples
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
   ============================================================================ */