/ Examples
scalescrollbar

Large List

Svelte implementation with vlist action + withCompression + withScrollbar plugins. Handles 100K–5M items with automatic scroll compression when total height exceeds the browser's 16.7M pixel limit.

Loading…

Compression activates automatically when the virtual height exceeds ~16.7 million pixels. The Svelte action integrates seamlessly with the builder's plugin system — compression logic is only loaded when you configure the compression plugin. 🎯

Source
// Large List — Svelte implementation with vlist action
// Uses builder pattern with compression + scrollbar plugins
// Demonstrates handling 100K–5M items with automatic scroll compression

import { vlist, onVListEvent } from "vlist-svelte";

// =============================================================================
// Constants
// =============================================================================

const ITEM_HEIGHT = 48;
const SIZES = {
  "100k": 100_000,
  "500k": 500_000,
  "1m": 1_000_000,
  "2m": 2_000_000,
  "5m": 5_000_000,
};

const COLORS = [
  "#667eea",
  "#764ba2",
  "#f093fb",
  "#f5576c",
  "#4facfe",
  "#43e97b",
  "#fa709a",
  "#fee140",
];

// =============================================================================
// Utilities
// =============================================================================

// Simple hash for consistent per-item values
const hash = (n) => {
  let h = (n + 1) * 2654435761;
  h ^= h >>> 16;
  return Math.abs(h);
};

// Generate items on the fly
const generateItems = (count) =>
  Array.from({ length: count }, (_, i) => ({
    id: i + 1,
    value: hash(i) % 100,
    hash: hash(i).toString(16).slice(0, 8).toUpperCase(),
    color: COLORS[i % COLORS.length],
  }));

// Item template
const itemTemplate = (item, index) => `
  <div class="item-row">
    <div class="item-color" style="background:${item.color}"></div>
    <div class="item-info">
      <span class="item-label">#${(index + 1).toLocaleString()}</span>
      <span class="item-hash">${item.hash}</span>
    </div>
    <div class="item-bar-wrap">
      <div class="item-bar" style="width:${item.value}%;background:${item.color}"></div>
    </div>
    <span class="item-value">${item.value}%</span>
  </div>
`;

// =============================================================================
// DOM references
// =============================================================================

const statsEl = document.getElementById("stats");
const compressionEl = document.getElementById("compression-info");
const scrollPosEl = document.getElementById("scroll-position");
const scrollDirEl = document.getElementById("scroll-direction");
const rangeEl = document.getElementById("visible-range");
const sizeButtons = document.getElementById("size-buttons");
const container = document.getElementById("list-container");

// =============================================================================
// State
// =============================================================================

let currentSize = "1m";
let action = null;
let listInstance = null;

// =============================================================================
// Stats functions
// =============================================================================

let statsRaf = null;

function scheduleStatsUpdate() {
  if (statsRaf) return;
  statsRaf = requestAnimationFrame(() => {
    statsRaf = null;
    updateStats(SIZES[currentSize]);
    updateCompressionInfo(SIZES[currentSize]);
  });
}

function updateStats(count, genTime, buildTime) {
  const domNodes = document.querySelectorAll(".vlist-item").length;
  const virtualized = ((1 - domNodes / count) * 100).toFixed(4);

  let html = `<strong>Total:</strong> ${count.toLocaleString()}`;
  html += ` · <strong>DOM:</strong> ${domNodes}`;
  html += ` · <strong>Virtualized:</strong> ${virtualized}%`;
  if (genTime !== undefined) {
    html += ` · <strong>Gen:</strong> ${genTime.toFixed(0)}ms`;
  }
  if (buildTime !== undefined) {
    html += ` · <strong>Build:</strong> ${buildTime.toFixed(0)}ms`;
  }
  statsEl.innerHTML = html;
}

function updateCompressionInfo(count) {
  const totalHeight = count * ITEM_HEIGHT;
  const maxHeight = 16_777_216; // browser limit ~16.7M px
  const isCompressed = totalHeight > maxHeight;
  const ratio = isCompressed ? (totalHeight / maxHeight).toFixed(1) : "1.0";

  let html = `<span class="compression-badge ${isCompressed ? "compression-badge--active" : "compression-badge--off"}">`;
  html += isCompressed ? "COMPRESSED" : "NATIVE";
  html += "</span>";
  html += ` <span class="compression-detail">`;
  html += `Virtual height: <strong>${(totalHeight / 1_000_000).toFixed(1)}M px</strong>`;
  html += ` · Ratio: <strong>${ratio}×</strong>`;
  html += ` · Limit: <strong>16.7M px</strong>`;
  html += `</span>`;
  compressionEl.innerHTML = html;
}

// =============================================================================
// Create / Recreate list
// =============================================================================

function createList(sizeKey) {
  // Destroy previous action
  if (action && action.destroy) {
    action.destroy();
    action = null;
    listInstance = null;
  }

  // Clear container
  container.innerHTML = "";

  const count = SIZES[sizeKey];
  const startTime = performance.now();
  const items = generateItems(count);
  const genTime = performance.now() - startTime;

  // Create vlist action
  action = vlist(container, {
    config: {
      ariaLabel: `${count.toLocaleString()} items list`,
      item: {
        height: ITEM_HEIGHT,
        template: itemTemplate,
      },
      items,
      plugins: [
        {
          name: "compression",
          config: {},
        },
        {
          name: "scrollbar",
          config: { autoHide: true },
        },
      ],
    },
    onInstance: (inst) => {
      const buildTime = performance.now() - startTime;

      // Store instance for navigation controls
      listInstance = inst;

      // Bind events
      onVListEvent(listInstance, "scroll", ({ scrollTop, direction }) => {
        scrollPosEl.textContent = `${Math.round(scrollTop).toLocaleString()}px`;
        scrollDirEl.textContent = direction === "up" ? "↑ up" : "↓ down";
        scheduleStatsUpdate();
      });

      onVListEvent(listInstance, "range:change", ({ range }) => {
        rangeEl.textContent = `${range.start.toLocaleString()} – ${range.end.toLocaleString()}`;
        scheduleStatsUpdate();
      });

      // Show initial stats
      updateStats(count, genTime, buildTime);
      updateCompressionInfo(count);
    },
  });
}

// =============================================================================
// Size selector buttons
// =============================================================================

sizeButtons.addEventListener("click", (e) => {
  const btn = e.target.closest("[data-size]");
  if (!btn) return;

  const size = btn.dataset.size;
  if (size === currentSize) return;

  currentSize = size;

  // Update active state
  sizeButtons.querySelectorAll("button").forEach((b) => {
    b.classList.toggle("panel-segmented__btn--active", b.dataset.size === size);
  });

  createList(size);
});

// =============================================================================
// Navigation controls
// =============================================================================

document.getElementById("btn-first").addEventListener("click", () => {
  listInstance?.scrollToIndex(0, "start");
});

document.getElementById("btn-middle").addEventListener("click", () => {
  listInstance?.scrollToIndex(Math.floor(SIZES[currentSize] / 2), "center");
});

document.getElementById("btn-last").addEventListener("click", () => {
  listInstance?.scrollToIndex(SIZES[currentSize] - 1, "end");
});

document.getElementById("btn-random").addEventListener("click", () => {
  const idx = Math.floor(Math.random() * SIZES[currentSize]);
  listInstance?.scrollToIndex(idx, "center");
  document.getElementById("scroll-index").value = idx;
});

document.getElementById("btn-go").addEventListener("click", () => {
  const idx = parseInt(document.getElementById("scroll-index").value, 10);
  if (Number.isNaN(idx)) return;
  const align = document.getElementById("scroll-align").value;
  listInstance?.scrollToIndex(
    Math.max(0, Math.min(idx, SIZES[currentSize] - 1)),
    align,
  );
});

document.getElementById("scroll-index").addEventListener("keydown", (e) => {
  if (e.key === "Enter") {
    e.preventDefault();
    document.getElementById("btn-go").click();
  }
});

document.getElementById("btn-smooth-top").addEventListener("click", () => {
  listInstance?.scrollToIndex(0, {
    align: "start",
    behavior: "smooth",
    duration: 800,
  });
});

document.getElementById("btn-smooth-bottom").addEventListener("click", () => {
  listInstance?.scrollToIndex(SIZES[currentSize] - 1, {
    align: "end",
    behavior: "smooth",
    duration: 800,
  });
});

// =============================================================================
// Initialise with 1M items
// =============================================================================

createList(currentSize);
<div class="container container">
    <header>
        <h1>Large List</h1>
        <p class="description">
            Svelte implementation with <code>vlist</code> action +
            <code>withCompression</code> + <code>withScrollbar</code> plugins.
            Handles 100K–5M items with automatic scroll compression when total
            height exceeds the browser's 16.7M pixel limit.
        </p>
    </header>

    <div class="stats" id="stats">Loading…</div>

    <div class="compression-bar" id="compression-info"></div>

    <div class="split-layout">
        <div class="split-main split-main--full">
            <div id="list-container"></div>
        </div>

        <aside class="split-panel">
            <!-- Size -->
            <section class="panel-section">
                <h3 class="panel-title">Size</h3>
                <div class="panel-row">
                    <div class="panel-segmented" id="size-buttons">
                        <button class="panel-segmented__btn" data-size="100k">
                            100K
                        </button>
                        <button class="panel-segmented__btn" data-size="500k">
                            500K
                        </button>
                        <button
                            class="panel-segmented__btn panel-segmented__btn--active"
                            data-size="1m"
                        >
                            1M
                        </button>
                        <button class="panel-segmented__btn" data-size="2m">
                            2M
                        </button>
                        <button class="panel-segmented__btn" data-size="5m">
                            5M
                        </button>
                    </div>
                </div>
            </section>

            <!-- Navigation -->
            <section class="panel-section">
                <h3 class="panel-title">Navigation</h3>

                <div class="panel-row">
                    <label class="panel-label" for="scroll-index"
                        >Scroll to index</label
                    >
                    <div class="panel-input-group">
                        <input
                            type="number"
                            id="scroll-index"
                            min="0"
                            value="0"
                            class="panel-input"
                        />
                        <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="btn-go"
                            class="panel-btn panel-btn--icon"
                            title="Go"
                        >
                            <svg
                                width="16"
                                height="16"
                                viewBox="0 0 24 24"
                                fill="currentColor"
                            >
                                <path
                                    d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
                                />
                            </svg>
                        </button>
                    </div>
                </div>

                <div class="panel-row">
                    <label class="panel-label">Quick jump</label>
                    <div class="panel-btn-group">
                        <button id="btn-first" class="panel-btn">First</button>
                        <button id="btn-middle" class="panel-btn">
                            Middle
                        </button>
                        <button id="btn-last" class="panel-btn">Last</button>
                        <button id="btn-random" class="panel-btn">
                            Random
                        </button>
                    </div>
                </div>

                <div class="panel-row">
                    <label class="panel-label">Smooth scroll</label>
                    <div class="panel-btn-group">
                        <button id="btn-smooth-top" class="panel-btn">
                            ↑ Top
                        </button>
                        <button id="btn-smooth-bottom" class="panel-btn">
                            ↓ Bottom
                        </button>
                    </div>
                </div>
            </section>

            <!-- Viewport -->
            <section class="panel-section">
                <h3 class="panel-title">Viewport</h3>
                <div class="panel-row">
                    <span class="panel-label">Scroll</span>
                    <span class="panel-value" id="scroll-position">0px</span>
                </div>
                <div class="panel-row">
                    <span class="panel-label">Direction</span>
                    <span class="panel-value" id="scroll-direction">–</span>
                </div>
                <div class="panel-row">
                    <span class="panel-label">Range</span>
                    <span class="panel-value" id="visible-range">–</span>
                </div>
            </section>
        </aside>
    </div>

    <footer>
        <p>
            Compression activates automatically when the virtual height exceeds
            ~16.7 million pixels. The Svelte action integrates seamlessly with
            the builder's plugin system — compression logic is only loaded when
            you configure the <code>compression</code> plugin. 🎯
        </p>
    </footer>
</div>
// Shared data and utilities for large-list example variants
// This file is imported by all framework implementations to avoid duplication

// =============================================================================
// Constants
// =============================================================================

export const ITEM_HEIGHT = 48;

export const SIZES = {
  "100k": 100_000,
  "500k": 500_000,
  "1m": 1_000_000,
  "2m": 2_000_000,
  "5m": 5_000_000,
};

export const COLORS = [
  "#667eea",
  "#764ba2",
  "#f093fb",
  "#f5576c",
  "#4facfe",
  "#43e97b",
  "#fa709a",
  "#fee140",
];

// =============================================================================
// Utilities
// =============================================================================

// Simple hash for consistent per-item values
export function hash(n) {
  let h = (n + 1) * 2654435761;
  h ^= h >>> 16;
  return Math.abs(h);
}

// Generate items on the fly
export function generateItems(count) {
  return Array.from({ length: count }, (_, i) => ({
    id: i + 1,
    value: hash(i) % 100,
    hash: hash(i).toString(16).slice(0, 8).toUpperCase(),
    color: COLORS[i % COLORS.length],
  }));
}

// =============================================================================
// Templates
// =============================================================================

// Item template
export const itemTemplate = (item, index) => `
  <div class="item-row">
    <div class="item-color" style="background:${item.color}"></div>
    <div class="item-info">
      <span class="item-label">#${(index + 1).toLocaleString()}</span>
      <span class="item-hash">${item.hash}</span>
    </div>
    <div class="item-bar-wrap">
      <div class="item-bar" style="width:${item.value}%;background:${item.color}"></div>
    </div>
    <span class="item-value">${item.value}%</span>
  </div>
`;

// =============================================================================
// Compression Info
// =============================================================================

export function getCompressionInfo(count, itemHeight = ITEM_HEIGHT) {
  const totalHeight = count * itemHeight;
  const maxHeight = 16_777_216; // browser limit ~16.7M px
  const isCompressed = totalHeight > maxHeight;
  const ratio = isCompressed ? (totalHeight / maxHeight).toFixed(1) : "1.0";

  return {
    isCompressed,
    virtualHeight: totalHeight,
    ratio,
  };
}

// Format virtualization percentage
export function calculateVirtualization(domNodes, total) {
  if (total > 0 && domNodes > 0) {
    return ((1 - domNodes / total) * 100).toFixed(4);
  }
  return "0.0000";
}
/* Builder Million Items — example-specific styles only
   Common styles (.container, h1, .description, .stats, footer)
   are provided by example/example.css using shell.css design tokens.
   Panel system (.split-layout, .split-panel, .panel-*)
   is also provided by example/example.css. */

/* List container */
#list-container {
    height: 600px;
    margin: 0 auto;
}

/* ============================================================================
   Size Selector
   ============================================================================ */

.size-selector {
    display: flex;
    gap: 6px;
    margin-bottom: 12px;
}

.size-btn {
    padding: 6px 16px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg-card);
    color: var(--text-muted);
    font-size: 13px;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
    transition: all 0.15s ease;
    flex: 1;
}

.size-btn:hover {
    border-color: var(--accent);
    color: var(--accent-text);
}

.size-btn--active {
    background: var(--accent);
    color: white;
    border-color: var(--accent);
}

/* ============================================================================
   Compression Bar
   ============================================================================ */

.compression-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 14px;
    margin-bottom: 16px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--bg-card);
    font-size: 13px;
    color: var(--text-muted);
}

.compression-badge {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 12px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    flex-shrink: 0;
}

.compression-badge--active {
    background: #ff6b6b;
    color: white;
}

.compression-badge--off {
    background: #51cf66;
    color: white;
}

.compression-detail {
    font-size: 12px;
    color: var(--text-muted);
}

.compression-detail strong {
    color: var(--text);
}

/* ============================================================================
   Item styles (inside list)
   ============================================================================ */

.item-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 0 16px;
    height: 100%;
}

.item-color {
    width: 8px;
    height: 28px;
    border-radius: 4px;
    flex-shrink: 0;
}

.item-info {
    display: flex;
    flex-direction: column;
    min-width: 80px;
    flex-shrink: 0;
}

.item-label {
    font-weight: 600;
    font-size: 13px;
    white-space: nowrap;
}

.item-hash {
    font-size: 11px;
    font-family: "SF Mono", Monaco, Menlo, monospace;
    color: var(--text-muted);
}

.item-bar-wrap {
    flex: 1;
    height: 6px;
    background: var(--border);
    border-radius: 3px;
    overflow: hidden;
    min-width: 0;
}

.item-bar {
    height: 100%;
    border-radius: 3px;
    transition: width 0.2s ease;
}

.item-value {
    font-size: 12px;
    font-weight: 600;
    min-width: 36px;
    text-align: right;
    flex-shrink: 0;
    color: var(--text-muted);
}

/* ============================================================================
   Responsive
   ============================================================================ */

@media (max-width: 820px) {
    #list-container {
        height: 400px;
    }

    .size-selector {
        flex-wrap: wrap;
    }

    .size-btn {
        flex: 0 0 auto;
        padding: 6px 12px;
    }

    .compression-bar {
        flex-wrap: wrap;
        gap: 6px;
    }
}