/ Examples

Horizontal Variable Width

Pure vanilla JavaScript demonstrating horizontal virtual scrolling with dynamic item widths. Each of the 1,000 items has a different width (120–340px) calculated via width: (index) => items[index].width. Featured items are wider. Only visible items are in the DOM, regardless of their varying widths.

Loading...

Uses orientation: 'horizontal' with a dynamic width function: width: (index) => items[index].width. The height cache efficiently handles variable widths using prefix sums for O(1) offset lookups and O(log n) binary search for index positioning. Notice how items of different widths scroll smoothly with perfect positioning. ✨

Source
// Horizontal Variable Width — Vanilla JavaScript
// Demonstrates orientation: 'horizontal' with dynamic item.width function

import { createVList } from "vlist";
import {
  items,
  itemTemplate,
  formatStatsHtml,
  ITEM_HEIGHT,
} from "../shared.js";

// Create horizontal virtual list with variable widths
const list = createVList({
  container: "#list-container",
  orientation: "horizontal",
  scroll: { wheel: true },
  ariaLabel: "Horizontal variable width carousel",
  item: {
    height: ITEM_HEIGHT,
    // Dynamic width function - each item has different width
    width: (index) => items[index].width,
    template: itemTemplate,
  },
  items,
});

// Update stats display
const statsEl = document.getElementById("stats");

const updateStats = () => {
  const domNodes = document.querySelectorAll(".vlist-item").length;
  statsEl.innerHTML = formatStatsHtml(domNodes, items.length);
};

list.on("scroll", updateStats);
list.on("range:change", updateStats);
updateStats();

// Log clicks
list.on("item:click", ({ item, index }) => {
  console.log(`Clicked: ${item.title} (${item.width}px) at index ${index}`);
});

// Control buttons
document.getElementById("btn-start")?.addEventListener("click", () => {
  list.scrollToIndex(0);
});

// Find first featured item
document.getElementById("btn-featured")?.addEventListener("click", () => {
  const featuredIndex = items.findIndex((item) => item.isFeatured);
  if (featuredIndex >= 0) {
    list.scrollToIndex(featuredIndex, "center");
  }
});

document.getElementById("btn-center")?.addEventListener("click", () => {
  list.scrollToIndex(500, "center");
});

document.getElementById("btn-end")?.addEventListener("click", () => {
  list.scrollToIndex(items.length - 1, "end");
});

document.getElementById("btn-smooth")?.addEventListener("click", () => {
  const current = list.getScrollPosition();
  const targetIndex = current < 100 ? 300 : 0;
  list.scrollToIndex(targetIndex, {
    align: "start",
    behavior: "smooth",
    duration: 1000,
  });
});
<div class="container">
    <header>
        <h1>Horizontal Variable Width</h1>
        <p class="description">
            Pure vanilla JavaScript demonstrating horizontal virtual scrolling
            with <strong>dynamic item widths</strong>. Each of the 1,000 items
            has a different width (120–340px) calculated via
            <code>width: (index) => items[index].width</code>. Featured items
            are wider. Only visible items are in the DOM, regardless of their
            varying widths.
        </p>
    </header>

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

    <div id="list-container"></div>

    <div class="controls">
        <button id="btn-start">⏮ Start</button>
        <button id="btn-featured">⭐ First Featured</button>
        <button id="btn-center">⏺ Center (500)</button>
        <button id="btn-end">⏭ End</button>
        <button id="btn-smooth">🎞 Smooth Scroll</button>
    </div>

    <footer>
        <p>
            Uses <code>orientation: 'horizontal'</code> with a
            <strong>dynamic width function</strong>:
            <code>width: (index) => items[index].width</code>. The height
            cache efficiently handles variable widths using prefix sums for
            O(1) offset lookups and O(log n) binary search for index
            positioning. Notice how items of different widths scroll smoothly
            with perfect positioning. ✨
        </p>
    </footer>
</div>
// Shared data and utilities for horizontal variable width example
// This file is imported by all framework implementations to avoid duplication

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

export const ITEM_COUNT = 1000;
export const ITEM_HEIGHT = 200;

// Color palette for cards
export const colors = [
  ["#667eea", "#764ba2"],
  ["#f093fb", "#f5576c"],
  ["#4facfe", "#00f2fe"],
  ["#43e97b", "#38f9d7"],
  ["#fa709a", "#fee140"],
  ["#a18cd1", "#fbc2eb"],
  ["#fccb90", "#d57eeb"],
  ["#e0c3fc", "#8ec5fc"],
  ["#f5576c", "#ff9a9e"],
  ["#667eea", "#00f2fe"],
];

export const categories = [
  "Photo",
  "Video",
  "Music",
  "Document",
  "Archive",
  "Code",
  "Design",
  "Text",
];

export const icons = ["📷", "🎬", "🎵", "📄", "📦", "💻", "🎨", "📝"];

// Width patterns - creates visual variety
const widthPatterns = [120, 180, 240, 160, 200, 140, 220, 190];

// =============================================================================
// Data Generation
// =============================================================================

// Generate items with variable widths
export const items = Array.from({ length: ITEM_COUNT }, (_, i) => {
  const categoryIndex = i % categories.length;
  const color = colors[i % colors.length];

  // Width varies based on pattern (simulating different content sizes)
  const width = widthPatterns[i % widthPatterns.length];

  // Some items are "featured" (wider)
  const isFeatured = i % 17 === 0;
  const finalWidth = isFeatured ? width + 100 : width;

  return {
    id: i + 1,
    title: isFeatured ? `⭐ Featured #${i + 1}` : `Item ${i + 1}`,
    category: categories[categoryIndex],
    icon: icons[categoryIndex],
    width: finalWidth,
    number: i + 1,
    gradientStart: color[0],
    gradientEnd: color[1],
    isFeatured,
  };
});

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

// Card template - width is set by vlist
export const itemTemplate = (item) => {
  const featuredClass = item.isFeatured ? "card--featured" : "";
  return `
    <div class="card ${featuredClass}" style="background: linear-gradient(135deg, ${item.gradientStart} 0%, ${item.gradientEnd} 100%);">
      <div class="card-icon">${item.icon}</div>
      <div class="card-content">
        <div class="card-title">${item.title}</div>
        <div class="card-category">${item.category}</div>
        <div class="card-width">${item.width}px</div>
      </div>
      ${item.isFeatured ? '<div class="ui-badge ui-badge--pill ui-badge--glass">Featured</div>' : ""}
    </div>
  `;
};

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

// Calculate stats
export function calculateStats(domNodes, total) {
  const saved = Math.round((1 - domNodes / total) * 100);

  // Calculate width statistics
  const widths = items.map((item) => item.width);
  const minWidth = Math.min(...widths);
  const maxWidth = Math.max(...widths);
  const avgWidth = Math.round(
    widths.reduce((a, b) => a + b, 0) / widths.length,
  );
  const totalWidth = widths.reduce((a, b) => a + b, 0);

  return { saved, minWidth, maxWidth, avgWidth, totalWidth };
}

// Format stats HTML
export function formatStatsHtml(domNodes, total) {
  const { saved, minWidth, maxWidth, avgWidth } = calculateStats(
    domNodes,
    total,
  );
  return `
    <span><strong>Total:</strong> ${total.toLocaleString()} items</span>
    <span><strong>DOM nodes:</strong> ${domNodes}</span>
    <span><strong>Memory saved:</strong> ${saved}%</span>
    <span><strong>Width:</strong> ${minWidth}–${maxWidth}px (avg ${avgWidth}px)</span>
  `;
}
/* Horizontal Variable Width Example — example-specific styles only
   Common styles (.container, h1, .description, .stats, footer)
   are provided by example/example.css using shell.css design tokens. */

/* List container - full width, responsive height */
#list-container {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    height: 200px;
    overflow: hidden;
}

/* Override vlist defaults for horizontal cards */
#list-container .vlist {
    border: none;
    border-radius: 12px;
    background: transparent;
    max-width: 100%;
    width: 100%;
    box-sizing: border-box;
    overflow-x: auto;
}

#list-container .vlist-viewport {
    max-width: 100%;
    width: 100% !important;
    overflow-x: auto;
}

#list-container .vlist-item {
    padding: 8px;
    border-right: none;
    cursor: pointer;
    /* Width is set dynamically by vlist */
}

/* Card styles */
.card {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    height: 100%;
    border-radius: 12px;
    color: #fff;
    text-align: center;
    padding: 16px 12px;
    position: relative;
    transition:
        transform 0.15s ease-out,
        box-shadow 0.15s ease-out;
}

.vlist-item:hover .card {
    transform: scale(1.03);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}

/* Featured cards - wider items */
.card--featured {
    border: 2px solid rgba(255, 255, 255, 0.3);
}

.card-icon {
    font-size: 32px;
    line-height: 1;
}

.card-content {
    display: flex;
    flex-direction: column;
    gap: 4px;
    width: 100%;
}

.card-title {
    font-weight: var(--fw-bold);
    font-size: 14px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

.card-category {
    font-size: 11px;
    opacity: 0.85;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.card-width {
    font-size: 10px;
    opacity: 0.7;
    font-family: var(--font-mono);
}

.card .ui-badge--glass {
    position: absolute;
    top: 8px;
    right: 8px;
}

/* Bottom controls */
.controls {
    margin-top: 16px;
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

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

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

    #list-container .vlist-item {
        padding: 6px;
    }

    .card {
        padding: 12px 10px;
        border-radius: 10px;
    }

    .card-icon {
        font-size: 28px;
    }

    .card-title {
        font-size: 13px;
    }
}

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

    #list-container .vlist-item {
        padding: 4px;
    }

    .card {
        padding: 10px 8px;
        gap: 6px;
        border-radius: 8px;
    }

    .card-icon {
        font-size: 24px;
    }

    .card-title {
        font-size: 12px;
    }

    .card-category {
        font-size: 10px;
    }

    .card-width {
        font-size: 9px;
    }

    .card .ui-badge--glass {
        font-size: 8px;
        padding: 2px 6px;
    }

    .controls {
        flex-direction: column;
        gap: 8px;
    }

    .controls button {
        width: 100%;
        font-size: 13px;
    }
}