Source
// Horizontal Scrolling — React implementation with useVList hook
// Demonstrates orientation: 'horizontal' with item.width
import { useState } from "react";
import { createRoot } from "react-dom/client";
import { scrollbar } from "vlist";
import { useVList, useVListEvent } from "vlist/react";
import {
items,
itemTemplate,
calculateStats,
ITEM_HEIGHT,
ITEM_WIDTH,
} from "../shared.js";
// =============================================================================
// App Component
// =============================================================================
function App() {
const [stats, setStats] = useState({
domNodes: 0,
saved: 0,
});
// Initialize vlist
const { containerRef, instanceRef } = useVList({
orientation: "horizontal",
scroll: { wheel: true },
ariaLabel: "Horizontal card carousel",
item: {
height: ITEM_HEIGHT,
width: ITEM_WIDTH,
template: itemTemplate,
},
items,
}, [scrollbar()]);
// Update stats
const updateStats = () => {
const domNodes = document.querySelectorAll(".vlist-item").length;
const { saved } = calculateStats(domNodes, items.length);
setStats({ domNodes, saved });
};
// Track scroll and range changes
useVListEvent(instanceRef, "scroll", updateStats);
useVListEvent(instanceRef, "range:change", updateStats);
// Track item clicks
useVListEvent(instanceRef, "item:click", ({ item, index }) => {
console.log(`Clicked: ${item.title} at index ${index}`);
});
// Navigation handlers
const scrollToStart = () => {
instanceRef.current?.scrollToIndex(0);
};
const scrollToCenter = () => {
instanceRef.current?.scrollToIndex(5000, "center");
};
const scrollToEnd = () => {
instanceRef.current?.scrollToIndex(items.length - 1, "end");
};
const smoothScroll = () => {
const current = instanceRef.current?.getScrollPosition() || 0;
const targetIndex = current < 100 ? 500 : 0;
instanceRef.current?.scrollToIndex(targetIndex, {
align: "start",
behavior: "smooth",
duration: 800,
});
};
return (
<div className="container">
<header>
<h1>Horizontal Scrolling</h1>
<p className="description">
React implementation with <code>useVList</code> hook. A horizontal
virtual list rendering 10,000 cards with{" "}
<code>orientation: 'horizontal'</code> and{" "}
<code>scroll.wheel: true</code>. Scroll with mouse wheel, trackpad
swipe, or the buttons below. Only visible items are in the DOM.
</p>
</header>
<div className="stats">
<span>
<strong>Total:</strong> {items.length.toLocaleString()}
</span>
<span>
<strong>DOM nodes:</strong> {stats.domNodes}
</span>
<span>
<strong>Memory saved:</strong> {stats.saved}%
</span>
</div>
<div ref={containerRef} id="list-container" />
<div className="controls">
<button onClick={scrollToStart}>⏮ Start</button>
<button onClick={scrollToCenter}>⏺ Center (5000)</button>
<button onClick={scrollToEnd}>⏭ End</button>
<button onClick={smoothScroll}>🎞 Smooth Scroll</button>
</div>
<footer>
<p>
Uses <code>orientation: 'horizontal'</code> with <code>item.width</code>{" "}
for the main axis and <code>item.height</code> for the cross axis.{" "}
<code>scroll.wheel: true</code> maps vertical mouse wheel to
horizontal scroll. Custom scrollbar renders at the bottom
automatically. ⚛️
</p>
</footer>
</div>
);
}
// =============================================================================
// Mount
// =============================================================================
createRoot(document.getElementById("react-root")!).render(<App />);
<div id="react-root"></div>
// Shared data and utilities for horizontal example variants
// This file is imported by all framework implementations to avoid duplication
// =============================================================================
// Constants
// =============================================================================
export const ITEM_COUNT = 10_000;
export const ITEM_HEIGHT = 220;
export const ITEM_WIDTH = 180;
// 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 emojis = ["🎵", "🎶", "🎸", "🥁", "🎹", "🎺", "🎻", "🎷", "🪗", "🪘"];
// =============================================================================
// Data Generation
// =============================================================================
// Generate card items
export const items = Array.from({ length: ITEM_COUNT }, (_, i) => {
const color = colors[i % colors.length];
return {
id: i + 1,
title: `Card ${i + 1}`,
subtitle: `Item #${(i + 1).toLocaleString()}`,
emoji: emojis[i % emojis.length],
gradientStart: color[0],
gradientEnd: color[1],
};
});
// =============================================================================
// Templates
// =============================================================================
// Card template
export const itemTemplate = (item) => `
<div class="card" style="background: linear-gradient(135deg, ${item.gradientStart} 0%, ${item.gradientEnd} 100%);">
<div class="card-emoji">${item.emoji}</div>
<div class="card-title">${item.title}</div>
<div class="card-subtitle">${item.subtitle}</div>
</div>
`;
// =============================================================================
// Utilities
// =============================================================================
// Calculate stats
export function calculateStats(domNodes, total) {
const saved = Math.round((1 - domNodes / total) * 100);
return { saved };
}
// Format stats HTML
export function formatStatsHtml(domNodes, total) {
const { saved } = calculateStats(domNodes, total);
return `
<span><strong>Total:</strong> ${total.toLocaleString()}</span>
<span><strong>DOM nodes:</strong> ${domNodes}</span>
<span><strong>Memory saved:</strong> ${saved}%</span>
`;
}
/* Horizontal Scrolling 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: 220px;
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;
}
/* 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;
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);
}
.card-emoji {
font-size: 36px;
line-height: 1;
}
.card-title {
font-weight: var(--fw-bold);
font-size: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.card-subtitle {
font-size: 12px;
opacity: 0.85;
white-space: nowrap;
}
/* Bottom controls */
.controls {
margin-top: 16px;
}
/* ============================================================================
Responsive
============================================================================ */
@media (max-width: 900px) {
#list-container {
height: 190px;
}
#list-container .vlist-item {
padding: 6px;
}
.card {
padding: 10px 8px;
border-radius: 10px;
}
.card-emoji {
font-size: 30px;
}
.card-title {
font-size: 14px;
}
}
@media (max-width: 720px) {
#list-container {
height: 160px;
}
#list-container .vlist-item {
padding: 4px;
}
.card {
padding: 8px 6px;
gap: 4px;
border-radius: 8px;
}
.card-emoji {
font-size: 24px;
}
.card-title {
font-size: 12px;
}
.card-subtitle {
font-size: 10px;
}
.controls {
flex-direction: column;
gap: 8px;
}
.controls button {
width: 100%;
font-size: 13px;
}
}