Patterns Hard
Virtual List
Windowed list rendering pattern for very large datasets using fixed-row virtualization.
Open in Lab
MCP
vanilla-js css react vue svelte
Targets: TS JS HTML React Vue Svelte
Code
* {
box-sizing: border-box;
}
:root {
--bg: #070d17;
--panel: #121b2b;
--border: rgba(255, 255, 255, 0.14);
--text: #e2e8f0;
--muted: #94a3b8;
--accent: #38bdf8;
}
body {
margin: 0;
font-family: "Space Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
background: radial-gradient(circle at top, #101e36, var(--bg));
color: var(--text);
min-height: 100vh;
}
.shell {
width: min(760px, calc(100% - 2rem));
margin: 2rem auto;
}
h1 {
margin-bottom: 0.35rem;
}
p {
margin-top: 0;
color: var(--muted);
}
.meta {
color: var(--muted);
font-size: 0.85rem;
margin-bottom: 0.75rem;
}
.viewport {
position: relative;
height: 460px;
overflow: auto;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--panel);
}
.spacer {
width: 100%;
}
.rows {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.row {
height: 44px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
padding: 0.6rem 0.8rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.row span:last-child {
color: var(--accent);
font-size: 0.8rem;
}(() => {
const TOTAL = 5000;
const ROW_HEIGHT = 44;
const OVERSCAN = 6;
const DATA = Array.from({ length: TOTAL }, (_, index) => ({
id: index + 1,
label: `Record #${index + 1}`,
group: `Group ${((index % 8) + 1).toString().padStart(2, "0")}`,
}));
const viewport = document.getElementById("viewport");
const spacer = document.getElementById("spacer");
const rows = document.getElementById("rows");
const meta = document.getElementById("meta");
spacer.style.height = `${TOTAL * ROW_HEIGHT}px`;
const render = () => {
const scrollTop = viewport.scrollTop;
const viewHeight = viewport.clientHeight;
const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
const end = Math.min(TOTAL, Math.ceil((scrollTop + viewHeight) / ROW_HEIGHT) + OVERSCAN);
rows.style.transform = `translateY(${start * ROW_HEIGHT}px)`;
const fragment = document.createDocumentFragment();
for (let index = start; index < end; index += 1) {
const row = DATA[index];
const el = document.createElement("div");
el.className = "row";
el.innerHTML = `<span>${row.label}</span><span>${row.group}</span>`;
fragment.appendChild(el);
}
rows.innerHTML = "";
rows.appendChild(fragment);
meta.textContent = `Rendering rows ${start + 1}-${end} of ${TOTAL}`;
};
viewport.addEventListener("scroll", render);
window.addEventListener("resize", render);
render();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Virtual List</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell">
<header>
<h1>Virtual List</h1>
<p>Windowed rendering of 5,000 rows.</p>
</header>
<div class="meta" id="meta"></div>
<section id="viewport" class="viewport" aria-label="Virtualized list" tabindex="0">
<div id="spacer" class="spacer"></div>
<div id="rows" class="rows"></div>
</section>
</main>
<script src="script.js"></script>
</body>
</html>import { useLayoutEffect, useMemo, useRef, useState } from "react";
const TOTAL = 5000;
const ROW_HEIGHT = 44;
const OVERSCAN = 8;
const DATA = Array.from({ length: TOTAL }, (_, index) => ({
id: index + 1,
label: `Record #${index + 1}`,
group: `Group ${((index % 8) + 1).toString().padStart(2, "0")}`,
}));
export default function VirtualListPattern() {
const viewportRef = useRef<HTMLDivElement | null>(null);
const [scrollTop, setScrollTop] = useState(0);
const [viewHeight, setViewHeight] = useState(460);
const [jumpTo, setJumpTo] = useState("250");
useLayoutEffect(() => {
const node = viewportRef.current;
if (!node) return;
const updateSize = () => setViewHeight(node.clientHeight);
updateSize();
const observer = new ResizeObserver(updateSize);
observer.observe(node);
return () => observer.disconnect();
}, []);
const windowed = useMemo(() => {
const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
const end = Math.min(TOTAL, Math.ceil((scrollTop + viewHeight) / ROW_HEIGHT) + OVERSCAN);
return { start, end, rows: DATA.slice(start, end) };
}, [scrollTop, viewHeight]);
const jump = () => {
const value = Number.parseInt(jumpTo, 10);
if (!Number.isFinite(value)) return;
const clamped = Math.min(TOTAL, Math.max(1, value));
const node = viewportRef.current;
if (!node) return;
node.scrollTo({ top: (clamped - 1) * ROW_HEIGHT, behavior: "smooth" });
};
return (
<section className="min-h-screen bg-[#0d1117] px-4 py-6 text-[#e6edf3]">
<div className="mx-auto max-w-4xl space-y-4">
<header className="rounded-2xl border border-[#30363d] bg-[#161b22] p-4">
<p className="text-xs font-bold uppercase tracking-wide text-[#8b949e]">Pattern</p>
<h1 className="mt-1 text-lg font-bold">Virtual List</h1>
<p className="mt-1 text-sm text-[#8b949e]">
Windowed rendering keeps a 5,000-row list responsive.
</p>
</header>
<div className="flex flex-wrap items-center gap-2 rounded-xl border border-[#30363d] bg-[#161b22] p-3">
<label className="text-xs font-semibold text-[#8b949e]">
Jump to row
<input
value={jumpTo}
onChange={(event) => setJumpTo(event.target.value)}
className="ml-2 w-24 rounded-md border border-[#30363d] bg-[#0d1117] px-2 py-1 text-xs text-[#e6edf3] outline-none focus:border-[#58a6ff]"
/>
</label>
<button
type="button"
onClick={jump}
className="rounded-md border border-[#58a6ff]/45 bg-[#58a6ff]/15 px-3 py-1.5 text-xs font-semibold text-[#c9e6ff] transition-colors hover:bg-[#58a6ff]/25"
>
Scroll
</button>
<p className="text-xs text-[#8b949e]">
Rendering rows{" "}
<span className="font-semibold text-[#e6edf3]">{windowed.start + 1}</span>
{" - "}
<span className="font-semibold text-[#e6edf3]">{windowed.end}</span> of{" "}
<span className="font-semibold text-[#e6edf3]">{TOTAL}</span>
</p>
</div>
<div
ref={viewportRef}
className="relative h-[62vh] min-h-[360px] overflow-auto rounded-2xl border border-[#30363d] bg-[#111827]"
onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}
>
<div style={{ height: TOTAL * ROW_HEIGHT }} />
<div
className="absolute left-0 top-0 w-full"
style={{ transform: `translateY(${windowed.start * ROW_HEIGHT}px)` }}
>
{windowed.rows.map((row) => (
<div
key={row.id}
className="grid h-11 grid-cols-[1fr_auto] items-center border-b border-white/5 px-3 text-sm odd:bg-white/[0.01]"
>
<div className="flex items-center gap-3">
<span className="w-14 font-mono text-xs text-[#8b949e]">#{row.id}</span>
<span className="text-[#dce6f2]">{row.label}</span>
</div>
<span className="font-mono text-xs text-sky-300">{row.group}</span>
</div>
))}
</div>
</div>
</div>
</section>
);
}<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue";
const TOTAL = 5000;
const ROW_HEIGHT = 44;
const OVERSCAN = 8;
const DATA = Array.from({ length: TOTAL }, (_, index) => ({
id: index + 1,
label: `Record #${index + 1}`,
group: `Group ${String((index % 8) + 1).padStart(2, "0")}`,
}));
const viewportEl = ref(null);
const scrollTop = ref(0);
const viewHeight = ref(460);
const jumpTo = ref("250");
let observer = null;
const startIdx = computed(() => Math.max(0, Math.floor(scrollTop.value / ROW_HEIGHT) - OVERSCAN));
const endIdx = computed(() =>
Math.min(TOTAL, Math.ceil((scrollTop.value + viewHeight.value) / ROW_HEIGHT) + OVERSCAN)
);
const rows = computed(() => DATA.slice(startIdx.value, endIdx.value));
onMounted(() => {
if (viewportEl.value) {
viewHeight.value = viewportEl.value.clientHeight;
observer = new ResizeObserver(() => {
viewHeight.value = viewportEl.value.clientHeight;
});
observer.observe(viewportEl.value);
}
});
onUnmounted(() => {
if (observer) observer.disconnect();
});
function onScroll(e) {
scrollTop.value = e.currentTarget.scrollTop;
}
function jump() {
const value = parseInt(jumpTo.value, 10);
if (!isFinite(value)) return;
const clamped = Math.min(TOTAL, Math.max(1, value));
if (viewportEl.value) {
viewportEl.value.scrollTo({ top: (clamped - 1) * ROW_HEIGHT, behavior: "smooth" });
}
}
</script>
<template>
<section class="min-h-screen bg-[#0d1117] px-4 py-6 text-[#e6edf3]">
<div class="mx-auto max-w-4xl space-y-4">
<header class="rounded-2xl border border-[#30363d] bg-[#161b22] p-4">
<p class="text-xs font-bold uppercase tracking-wide text-[#8b949e]">Pattern</p>
<h1 class="mt-1 text-lg font-bold">Virtual List</h1>
<p class="mt-1 text-sm text-[#8b949e]">
Windowed rendering keeps a 5,000-row list responsive.
</p>
</header>
<div class="flex flex-wrap items-center gap-2 rounded-xl border border-[#30363d] bg-[#161b22] p-3">
<label class="text-xs font-semibold text-[#8b949e]">
Jump to row
<input
v-model="jumpTo"
class="ml-2 w-24 rounded-md border border-[#30363d] bg-[#0d1117] px-2 py-1 text-xs text-[#e6edf3] outline-none focus:border-[#58a6ff]"
/>
</label>
<button
type="button"
@click="jump"
class="rounded-md border border-[#58a6ff]/45 bg-[#58a6ff]/15 px-3 py-1.5 text-xs font-semibold text-[#c9e6ff] transition-colors hover:bg-[#58a6ff]/25"
>
Scroll
</button>
<p class="text-xs text-[#8b949e]">
Rendering rows
<span class="font-semibold text-[#e6edf3]">{{ startIdx + 1 }}</span>
-
<span class="font-semibold text-[#e6edf3]">{{ endIdx }}</span> of
<span class="font-semibold text-[#e6edf3]">{{ TOTAL }}</span>
</p>
</div>
<div
ref="viewportEl"
class="relative h-[62vh] min-h-[360px] overflow-auto rounded-2xl border border-[#30363d] bg-[#111827]"
@scroll="onScroll"
>
<div :style="{ height: TOTAL * ROW_HEIGHT + 'px' }" />
<div
class="absolute left-0 top-0 w-full"
:style="{ transform: `translateY(${startIdx * ROW_HEIGHT}px)` }"
>
<div
v-for="row in rows"
:key="row.id"
class="grid h-11 grid-cols-[1fr_auto] items-center border-b border-white/5 px-3 text-sm odd:bg-white/[0.01]"
>
<div class="flex items-center gap-3">
<span class="w-14 font-mono text-xs text-[#8b949e]">#{{ row.id }}</span>
<span class="text-[#dce6f2]">{{ row.label }}</span>
</div>
<span class="font-mono text-xs text-sky-300">{{ row.group }}</span>
</div>
</div>
</div>
</div>
</section>
</template><script>
import { onMount, onDestroy } from "svelte";
const TOTAL = 5000;
const ROW_HEIGHT = 44;
const OVERSCAN = 8;
const DATA = Array.from({ length: TOTAL }, (_, index) => ({
id: index + 1,
label: `Record #${index + 1}`,
group: `Group ${String((index % 8) + 1).padStart(2, "0")}`,
}));
let viewportEl;
let scrollTop = 0;
let viewHeight = 460;
let jumpTo = "250";
let observer;
$: startIdx = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
$: endIdx = Math.min(TOTAL, Math.ceil((scrollTop + viewHeight) / ROW_HEIGHT) + OVERSCAN);
$: rows = DATA.slice(startIdx, endIdx);
onMount(() => {
if (viewportEl) {
viewHeight = viewportEl.clientHeight;
observer = new ResizeObserver(() => {
viewHeight = viewportEl.clientHeight;
});
observer.observe(viewportEl);
}
});
onDestroy(() => {
if (observer) observer.disconnect();
});
function onScroll(e) {
scrollTop = e.currentTarget.scrollTop;
}
function jump() {
const value = parseInt(jumpTo, 10);
if (!isFinite(value)) return;
const clamped = Math.min(TOTAL, Math.max(1, value));
if (viewportEl) {
viewportEl.scrollTo({ top: (clamped - 1) * ROW_HEIGHT, behavior: "smooth" });
}
}
</script>
<section class="min-h-screen bg-[#0d1117] px-4 py-6 text-[#e6edf3]">
<div class="mx-auto max-w-4xl space-y-4">
<header class="rounded-2xl border border-[#30363d] bg-[#161b22] p-4">
<p class="text-xs font-bold uppercase tracking-wide text-[#8b949e]">Pattern</p>
<h1 class="mt-1 text-lg font-bold">Virtual List</h1>
<p class="mt-1 text-sm text-[#8b949e]">
Windowed rendering keeps a 5,000-row list responsive.
</p>
</header>
<div class="flex flex-wrap items-center gap-2 rounded-xl border border-[#30363d] bg-[#161b22] p-3">
<label class="text-xs font-semibold text-[#8b949e]">
Jump to row
<input
bind:value={jumpTo}
class="ml-2 w-24 rounded-md border border-[#30363d] bg-[#0d1117] px-2 py-1 text-xs text-[#e6edf3] outline-none focus:border-[#58a6ff]"
/>
</label>
<button
type="button"
on:click={jump}
class="rounded-md border border-[#58a6ff]/45 bg-[#58a6ff]/15 px-3 py-1.5 text-xs font-semibold text-[#c9e6ff] transition-colors hover:bg-[#58a6ff]/25"
>
Scroll
</button>
<p class="text-xs text-[#8b949e]">
Rendering rows
<span class="font-semibold text-[#e6edf3]">{startIdx + 1}</span>
{" - "}
<span class="font-semibold text-[#e6edf3]">{endIdx}</span> of
<span class="font-semibold text-[#e6edf3]">{TOTAL}</span>
</p>
</div>
<div
bind:this={viewportEl}
class="relative h-[62vh] min-h-[360px] overflow-auto rounded-2xl border border-[#30363d] bg-[#111827]"
on:scroll={onScroll}
>
<div style="height: {TOTAL * ROW_HEIGHT}px;" />
<div
class="absolute left-0 top-0 w-full"
style="transform: translateY({startIdx * ROW_HEIGHT}px);"
>
{#each rows as row (row.id)}
<div
class="grid h-11 grid-cols-[1fr_auto] items-center border-b border-white/5 px-3 text-sm odd:bg-white/[0.01]"
>
<div class="flex items-center gap-3">
<span class="w-14 font-mono text-xs text-[#8b949e]">#{row.id}</span>
<span class="text-[#dce6f2]">{row.label}</span>
</div>
<span class="font-mono text-xs text-sky-300">{row.group}</span>
</div>
{/each}
</div>
</div>
</div>
</section>Virtual List
A high-performance rendering pattern for thousands of rows where only visible items are mounted.
Features
- Fixed-height row virtualization
- Overscan buffer for smooth scrolling
- Absolute-positioned windowed rows
- Row index and metadata rendering