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...
Source
// 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;
}
}