coreinvert
Variable Sizes
Social feed where item sizes are unknown upfront.
Switch between Mode A (pre-measure all items at
init via hidden DOM element) and Mode B (estimated
size, let ResizeObserver measure on the fly and correct
scroll position).
Source
// Variable Sizes — Social feed with Mode A / Mode B size handling
// Demonstrates both approaches to variable-height items:
// A · Pre-measure all items via hidden DOM element (size function)
// B · Auto-size via estimatedHeight + ResizeObserver
// Uses split-layout pattern with side panel, mode toggle, and footer stats.
import { vlist } from "vlist";
import { createStats } from "../stats.js";
import { initModeToggle } from "./controls.js";
import { getAllPosts } from "../../src/api/posts.js";
// =============================================================================
// Constants
// =============================================================================
const TOTAL_POSTS = 5000;
const ESTIMATED_POST_HEIGHT = 200;
// =============================================================================
// Data — generated from API module (deterministic, same every time)
// =============================================================================
export const items = getAllPosts(TOTAL_POSTS);
export let list = null;
export let currentMode = "b"; // "a" | "b"
export function setCurrentMode(v) {
currentMode = v;
}
// =============================================================================
// Templates
// =============================================================================
const renderPostHTML = (item) => `
<article class="post-card">
<div class="post-card__header">
<img class="post-card__avatar" src="${item.avatarUrl}" alt="${item.user}" loading="lazy" />
<div class="post-card__meta">
<span class="post-card__user">${item.user}</span>
<span class="post-card__time">${item.time}</span>
</div>
</div>
<div class="post-card__title">${item.title}</div>
<div class="post-card__body">${item.body}</div>
<div class="post-card__actions">
<span class="post-card__action"><span class="post-card__action-icon">❤️</span> ${item.likes}</span>
<span class="post-card__action"><span class="post-card__action-icon">💬</span> ${item.comments}</span>
<span class="post-card__action"><span class="post-card__action-icon">🔄</span> ${item.shares}</span>
</div>
</article>
`;
const renderItem = (item) => renderPostHTML(item);
// =============================================================================
// Mode A — Pre-measure all items via hidden DOM element
// =============================================================================
/**
* Measure the actual rendered height of every item by inserting its HTML
* into a hidden element that matches the list's inner width.
*
* We cache by body text so items with identical content share a single
* measurement. For 5 000 items with ~12 unique body texts this means
* ~12 actual DOM measurements instead of 5 000.
*/
const measureSizes = (itemList, container) => {
const measurer = document.createElement("div");
measurer.style.cssText =
"position:absolute;top:0;left:0;visibility:hidden;pointer-events:none;" +
`width:${container.offsetWidth}px;`;
document.body.appendChild(measurer);
const cache = new Map();
let uniqueCount = 0;
for (const item of itemList) {
const key = item.body;
if (cache.has(key)) {
item.size = cache.get(key);
continue;
}
measurer.innerHTML = renderPostHTML(item);
const measured = measurer.firstElementChild.offsetHeight;
item.size = measured;
cache.set(key, measured);
uniqueCount++;
}
measurer.remove();
return uniqueCount;
};
// =============================================================================
// DOM references
// =============================================================================
const containerEl = document.getElementById("list-container");
// Measurement info
const infoStrategyEl = document.getElementById("info-strategy");
const infoInitEl = document.getElementById("info-init");
const infoUniqueEl = document.getElementById("info-unique");
// Footer right side
const ftModeEl = document.getElementById("ft-mode");
const ftEstimateEl = document.getElementById("ft-estimate");
// =============================================================================
// Stats — shared footer (progress, velocity, visible/total)
// =============================================================================
export const stats = createStats({
getList: () => list,
getTotal: () => items.length,
getItemHeight: () => ESTIMATED_POST_HEIGHT,
container: "#list-container",
});
// =============================================================================
// Create / Recreate list — called when mode changes
// =============================================================================
let firstVisibleIndex = 0;
export function createList() {
if (list) {
list.destroy();
list = null;
}
containerEl.innerHTML = "";
let initTime = 0;
let uniqueSizes = 0;
if (currentMode === "a") {
// Mode A: pre-measure all items, then use size function
const start = performance.now();
if (items.length > 0) {
uniqueSizes = measureSizes(items, containerEl);
}
initTime = performance.now() - start;
list = vlist({
container: containerEl,
ariaLabel: "Social feed",
items,
item: {
height: (index) => items[index]?.size ?? ESTIMATED_POST_HEIGHT,
template: renderItem,
},
}).build();
} else {
// Mode B: estimated size, auto-measured by ResizeObserver
const start = performance.now();
list = vlist({
container: containerEl,
ariaLabel: "Social feed",
items,
item: {
estimatedHeight: ESTIMATED_POST_HEIGHT,
template: renderItem,
},
}).build();
initTime = performance.now() - start;
}
// Wire stats events
list.on("scroll", stats.scheduleUpdate);
list.on("range:change", ({ range }) => {
firstVisibleIndex = range.start;
stats.scheduleUpdate();
});
list.on("velocity:change", ({ velocity }) => stats.onVelocity(velocity));
// Restore scroll position
if (firstVisibleIndex > 0) {
list.scrollToIndex(firstVisibleIndex, "start");
}
stats.update();
updatePanelInfo(initTime, uniqueSizes);
}
// =============================================================================
// Panel info — measurement section + footer right
// =============================================================================
function updatePanelInfo(initTime, uniqueSizes) {
const modeLabel = currentMode === "a" ? "Mode A" : "Mode B";
if (ftModeEl) ftModeEl.textContent = modeLabel;
if (ftEstimateEl) {
ftEstimateEl.textContent =
currentMode === "a" ? "pre-measured" : `${ESTIMATED_POST_HEIGHT}px`;
}
if (infoStrategyEl) {
infoStrategyEl.textContent =
currentMode === "a" ? "height: (i) => px" : "estimatedHeight";
}
if (infoInitEl) {
infoInitEl.textContent = `${initTime.toFixed(0)}ms`;
}
if (infoUniqueEl) {
infoUniqueEl.textContent = currentMode === "a" ? String(uniqueSizes) : "–";
}
}
// =============================================================================
// Initialise
// =============================================================================
initModeToggle();
createList();
/* Variable Sizes — example-specific styles only
Common styles (.container, h1, .description, .stats, footer)
are provided by examples/examples.css using shell.css design tokens.
UI components (.split-layout, .split-panel, .ui-*)
is also provided by examples/examples.css. */
/* ============================================================================
List container
============================================================================ */
#list-container {
height: 600px;
background: var(--vlist-bg);
border-radius: 2px;
}
/* ============================================================================
vlist item overrides
============================================================================ */
#list-container .vlist {
background-color: #4e6070;
}
#list-container .vlist-item {
padding: 0;
display: flex;
margin: 10px 20px;
background-color: transparent;
}
/* ============================================================================
Post Card — social feed card with avatar, title, body, actions
============================================================================ */
.post-card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px 20px;
background: #ffffff;
border-radius: 12px;
width: 100%;
height: calc(100% - 12px);
box-sizing: border-box;
}
/* --- Header: avatar + name/time --- */
.post-card__header {
display: flex;
align-items: center;
gap: 10px;
}
.post-card__avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
background: var(--vlist-border);
}
.post-card__meta {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.post-card__user {
font-weight: 600;
font-size: 15px;
color: var(--vlist-text);
line-height: 1.3;
}
.post-card__time {
font-size: 12px;
color: var(--vlist-text-muted);
line-height: 1.3;
}
/* --- Title --- */
.post-card__title {
font-size: 16px;
font-weight: 700;
color: var(--vlist-text);
line-height: 1.35;
padding-top: 2px;
}
/* --- Body text --- */
.post-card__body {
font-size: 14px;
color: var(--vlist-text-muted);
line-height: 1.55;
word-break: break-word;
}
/* --- Action bar --- */
.post-card__actions {
display: flex;
align-items: center;
gap: 16px;
padding-top: 4px;
border-top: 1px solid var(--vlist-text-dim);
}
.post-card__action {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 500;
color: var(--text-dim);
cursor: default;
user-select: none;
}
.post-card__action-icon {
font-size: 14px;
line-height: 1;
}
<div class="container">
<header>
<h1>Variable Sizes</h1>
<p class="description">
Social feed where item sizes are <strong>unknown upfront</strong>.
Switch between <strong>Mode A</strong> (pre-measure all items at
init via hidden DOM element) and <strong>Mode B</strong> (estimated
size, let <code>ResizeObserver</code> measure on the fly and correct
scroll position).
</p>
</header>
<div class="split-layout">
<div class="split-main">
<div id="list-container"></div>
</div>
<aside class="split-panel">
<!-- Mode -->
<section class="ui-section">
<h3 class="ui-title">Mode</h3>
<div class="ui-row">
<div class="ui-segmented" id="mode-toggle">
<button class="ui-segmented__btn" data-mode="a">
A · Pre-measure
</button>
<button
class="ui-segmented__btn ui-segmented__btn--active"
data-mode="b"
>
B · Auto-size
</button>
</div>
</div>
</section>
<!-- Measurement -->
<section class="ui-section" id="section-measurement">
<h3 class="ui-title">Measurement</h3>
<div class="ui-row">
<span class="ui-label">Strategy</span>
<span class="ui-value" id="info-strategy"
>estimatedHeight</span
>
</div>
<div class="ui-row">
<span class="ui-label">Init time</span>
<span class="ui-value" id="info-init">–</span>
</div>
<div class="ui-row">
<span class="ui-label">Unique sizes</span>
<span class="ui-value" id="info-unique">–</span>
</div>
</section>
<!-- Navigation -->
<section class="ui-section">
<h3 class="ui-title">Navigation</h3>
<div class="ui-row">
<div class="ui-btn-group">
<button
id="jump-top"
class="ui-btn ui-btn--icon"
title="Top"
>
<i class="icon icon--up"></i>
</button>
<button
id="jump-middle"
class="ui-btn ui-btn--icon"
title="Middle"
>
<i class="icon icon--center"></i>
</button>
<button
id="jump-bottom"
class="ui-btn ui-btn--icon"
title="Bottom"
>
<i class="icon icon--down"></i>
</button>
<button
id="jump-random"
class="ui-btn ui-btn--icon"
title="Random"
>
<i class="icon icon--shuffle"></i>
</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">
<strong id="ft-mode">Mode B</strong>
</span>
<span class="example-footer__stat">
<strong id="ft-estimate">200px</strong>
<span class="example-footer__unit">estimate</span>
</span>
</div>
</footer>
</div>