UI Components Hard
Virtualized Table
Keep a dense table responsive by windowing rows while headers stay outside the scroll region.
Open in Lab
MCP
html css javascript react
Targets: TS JS HTML React
Code
*, *::before, *::after { box-sizing: border-box; }
:root {
--bg: #0b1020;
--panel: #121a2d;
--panel-2: #18233b;
--line: #2b3d62;
--text: #eef2ff;
--muted: #a8b5d1;
--accent: #78a9ff;
--ok: #3ecf8e;
--warn: #f0b849;
--bad: #f2647a;
--row-h: 44px;
--cols: 74px minmax(140px, 1.4fr) minmax(104px, 1fr) 116px 92px 104px;
}
body {
margin: 0;
min-height: 100vh;
padding: clamp(1rem, 4vw, 3rem);
background: radial-gradient(120% 90% at 50% 0%, #141d38 0%, var(--bg) 60%);
color: var(--text);
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
button, input, select { font: inherit; }
.demo { width: min(940px, 100%); margin: auto; }
.vt-head {
display: flex; flex-wrap: wrap; gap: 12px;
align-items: flex-end; justify-content: space-between;
margin-bottom: 18px;
}
.vt-head h1 { margin: 0; font-size: 20px; letter-spacing: -.01em; }
.vt-sub { margin: 4px 0 0; color: var(--muted); font-size: 13px; }
.vt-stats {
display: flex; gap: 14px; font-size: 12px; color: var(--muted);
font-variant-numeric: tabular-nums;
}
.vt-stats b { color: var(--accent); font-weight: 600; }
.vt-toolbar { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 14px; }
.vt-field { display: flex; flex-direction: column; gap: 5px; flex: 1 1 220px; }
.vt-field--sm { flex: 0 1 160px; }
.vt-field > span {
font-size: 11px; text-transform: uppercase; letter-spacing: .07em; color: var(--muted);
}
.vt-field input, .vt-field select {
appearance: none;
background: var(--panel-2);
border: 1px solid var(--line);
color: var(--text);
border-radius: 10px;
padding: 9px 11px;
transition: border-color .15s ease, box-shadow .15s ease;
}
.vt-field input:focus, .vt-field select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(120, 169, 255, .2);
}
.vt-table {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 20px 60px -28px #000c;
}
.vt-row {
display: grid;
grid-template-columns: var(--cols);
align-items: center;
gap: 12px;
padding: 0 16px;
height: var(--row-h);
font-variant-numeric: tabular-nums;
}
.vt-row--header {
height: 40px;
background: var(--panel-2);
border-bottom: 1px solid var(--line);
font-size: 11px;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--muted);
}
.vt-row .num { text-align: right; }
.vt-viewport {
height: calc(var(--row-h) * 9);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-color: #33415f transparent;
}
.vt-viewport:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.vt-viewport::-webkit-scrollbar { width: 10px; }
.vt-viewport::-webkit-scrollbar-thumb {
background: #33415f; border-radius: 8px; border: 3px solid var(--panel);
}
.vt-spacer { position: relative; }
.vt-window { position: absolute; inset: 0 0 auto 0; will-change: transform; }
.vt-row--data { border-bottom: 1px solid rgba(43, 61, 98, .6); }
.vt-row--data:nth-child(even) { background: rgba(255, 255, 255, .018); }
.vt-row--data:hover { background: rgba(120, 169, 255, .07); }
.vt-row--data[aria-selected="true"] {
background: rgba(120, 169, 255, .14);
box-shadow: inset 2px 0 0 var(--accent);
}
.vt-id { color: var(--muted); font-size: 12px; }
.vt-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vt-region { color: var(--muted); font-size: 13px; }
.vt-badge {
justify-self: start;
font-size: 11px; font-weight: 600;
padding: 3px 9px; border-radius: 999px;
border: 1px solid currentColor;
}
.vt-badge.ok { color: var(--ok); background: rgba(62, 207, 142, .1); }
.vt-badge.warn { color: var(--warn); background: rgba(240, 184, 73, .1); }
.vt-badge.bad { color: var(--bad); background: rgba(242, 100, 122, .1); }
.vt-empty { margin: 14px 0 0; color: var(--muted); text-align: center; font-size: 13px; }
@media (max-width: 640px) {
:root { --cols: 62px minmax(110px, 1.4fr) 96px 96px; }
.vt-row > :nth-child(3), .vt-row > :nth-child(5) { display: none; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; scroll-behavior: auto !important; }
}/* Virtualized Table — manual windowing over 10,000 rows.
Only ~visible rows + overscan exist in the DOM at any time. */
(() => {
const viewport = document.getElementById("vt-viewport");
const spacer = document.getElementById("vt-spacer");
const windowEl = document.getElementById("vt-window");
const filterInput = document.getElementById("vt-filter");
const sortSelect = document.getElementById("vt-sort");
const emptyEl = document.getElementById("vt-empty");
const nodesEl = document.getElementById("vt-nodes");
const rangeEl = document.getElementById("vt-range");
const totalEl = document.getElementById("vt-total");
if (!viewport || !windowEl) return;
const ROW_H = 44; // must match --row-h
const OVERSCAN = 4;
const TOTAL = 10000;
/* ---- deterministic dataset (no network, no deps) ---- */
const REGIONS = ["Nordic", "Iberia", "Benelux", "Levant", "Andes", "Pacific NW", "Sahel"];
const WORDS = ["Helio", "Vertex", "Quanta", "Lumen", "Orbit", "Cinder", "Nimbus", "Talos", "Auric", "Kestrel"];
let seed = 1337;
const rnd = () => (seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff;
const DATA = Array.from({ length: TOTAL }, (_, i) => {
const name = `${WORDS[Math.floor(rnd() * WORDS.length)]} ${WORDS[Math.floor(rnd() * WORDS.length)]}`;
const uptime = 96 + rnd() * 4;
return {
id: i + 1,
name,
region: REGIONS[Math.floor(rnd() * REGIONS.length)],
revenue: Math.round(5000 + rnd() * 995000),
uptime,
status: uptime > 99.5 ? "ok" : uptime > 98 ? "warn" : "bad",
};
});
const money = new Intl.NumberFormat("en-US", {
style: "currency", currency: "USD", maximumFractionDigits: 0,
});
const STATUS_LABEL = { ok: "Healthy", warn: "Degraded", bad: "At risk" };
/* ---- derived view (filter + sort) ---- */
let view = DATA;
let selectedIndex = -1;
function recompute() {
const q = filterInput.value.trim().toLowerCase();
view = q
? DATA.filter((r) => r.name.toLowerCase().includes(q) || r.region.toLowerCase().includes(q))
: DATA;
const key = sortSelect.value;
if (key !== "id") {
view = view.slice().sort((a, b) =>
key === "name" ? a.name.localeCompare(b.name) : b[key] - a[key]
);
}
selectedIndex = -1;
spacer.style.height = `${view.length * ROW_H}px`;
totalEl.textContent = view.length.toLocaleString();
emptyEl.hidden = view.length > 0;
viewport.scrollTop = 0;
// force: the window range is often unchanged (scrollTop already 0), but the
// underlying rows changed — a non-forced render would early-out and keep stale rows.
render(true);
}
/* ---- the windowing core ---- */
let lastStart = -1;
let lastEnd = -1;
function render(force) {
const scrollTop = viewport.scrollTop;
const visible = Math.ceil(viewport.clientHeight / ROW_H);
const start = Math.max(0, Math.floor(scrollTop / ROW_H) - OVERSCAN);
const end = Math.min(view.length, start + visible + OVERSCAN * 2);
if (!force && start === lastStart && end === lastEnd) return;
lastStart = start;
lastEnd = end;
windowEl.style.transform = `translateY(${start * ROW_H}px)`;
let html = "";
for (let i = start; i < end; i++) {
const r = view[i];
html +=
`<div class="vt-row vt-row--data" role="row" data-index="${i}"` +
` aria-rowindex="${i + 1}" aria-selected="${i === selectedIndex}">` +
`<span class="vt-id" role="cell">#${r.id}</span>` +
`<span class="vt-name" role="cell">${r.name}</span>` +
`<span class="vt-region" role="cell">${r.region}</span>` +
`<span class="num" role="cell">${money.format(r.revenue)}</span>` +
`<span class="num" role="cell">${r.uptime.toFixed(2)}%</span>` +
`<span role="cell"><span class="vt-badge ${r.status}">${STATUS_LABEL[r.status]}</span></span>` +
`</div>`;
}
windowEl.innerHTML = html;
nodesEl.textContent = end - start;
rangeEl.textContent = view.length ? `${start + 1}–${end}` : "0–0";
}
/* ---- scroll: coalesce into one rAF per frame ---- */
let ticking = false;
viewport.addEventListener("scroll", () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { ticking = false; render(); });
}, { passive: true });
new ResizeObserver(() => render(true)).observe(viewport);
/* ---- selection + keyboard navigation ---- */
function select(index) {
if (!view.length) return;
selectedIndex = Math.max(0, Math.min(view.length - 1, index));
const top = selectedIndex * ROW_H;
const bottom = top + ROW_H;
if (top < viewport.scrollTop) viewport.scrollTop = top;
else if (bottom > viewport.scrollTop + viewport.clientHeight) {
viewport.scrollTop = bottom - viewport.clientHeight;
}
render(true);
}
windowEl.addEventListener("click", (e) => {
const row = e.target.closest("[data-index]");
if (row) select(Number(row.dataset.index));
});
viewport.addEventListener("keydown", (e) => {
const page = Math.max(1, Math.floor(viewport.clientHeight / ROW_H) - 1);
const base = selectedIndex < 0 ? Math.floor(viewport.scrollTop / ROW_H) - 1 : selectedIndex;
const moves = {
ArrowDown: base + 1,
ArrowUp: base - 1,
PageDown: base + page,
PageUp: base - page,
Home: 0,
End: view.length - 1,
};
if (!(e.key in moves)) return;
e.preventDefault();
select(moves[e.key]);
});
filterInput.addEventListener("input", recompute);
sortSelect.addEventListener("change", recompute);
recompute();
})();<link rel="stylesheet" href="style.css">
<main class="demo" data-demo="Virtualized Table">
<header class="vt-head">
<div>
<h1>Virtualized Table</h1>
<p class="vt-sub">10,000 rows — only the visible window exists in the DOM.</p>
</div>
<div class="vt-stats" role="status" aria-live="polite">
<span><b id="vt-nodes">0</b> row nodes</span>
<span>rows <b id="vt-range">0–0</b> of <b id="vt-total">0</b></span>
</div>
</header>
<div class="vt-toolbar">
<label class="vt-field">
<span>Filter by account or region</span>
<input id="vt-filter" type="search" placeholder="e.g. Nordic" autocomplete="off">
</label>
<label class="vt-field vt-field--sm">
<span>Sort by</span>
<select id="vt-sort">
<option value="id">ID</option>
<option value="name">Account</option>
<option value="revenue">Revenue</option>
<option value="uptime">Uptime</option>
</select>
</label>
</div>
<div class="vt-table" role="table" aria-label="Account performance">
<div class="vt-row vt-row--header" role="row">
<span role="columnheader">ID</span>
<span role="columnheader">Account</span>
<span role="columnheader">Region</span>
<span role="columnheader" class="num">Revenue</span>
<span role="columnheader" class="num">Uptime</span>
<span role="columnheader">Status</span>
</div>
<div class="vt-viewport" id="vt-viewport" tabindex="0"
aria-label="Table rows. Scrollable; use arrow keys, Page Up, Page Down, Home and End.">
<div class="vt-spacer" id="vt-spacer">
<div class="vt-window" id="vt-window" role="rowgroup"></div>
</div>
</div>
</div>
<p class="vt-empty" id="vt-empty" hidden>No rows match that filter.</p>
</main>
<script src="script.js"></script>import { useMemo, useState } from "react";
export function VirtualScrollTable() {
const [offset, setOffset] = useState(0);
const rows = useMemo(() => Array.from({ length: 20 }, (_, index) => index + offset), [offset]);
return (
<section className="demo">
<h2>Virtualized Table</h2>
<div
style={{ height: 260, overflow: "auto" }}
onScroll={(event) => setOffset(Math.floor(event.currentTarget.scrollTop / 42))}
>
{rows.map((row) => (
<div key={row} style={{ height: 42, padding: 10, borderBottom: "1px solid #2b3d62" }}>
Row {row + 1}
</div>
))}
</div>
</section>
);
}Virtualized Table
Keep a dense table responsive by windowing rows while headers stay outside the scroll region.
Support notes
Manual windowing keeps DOM work proportional to the viewport. IntersectionObserver is used for the feed sentinel; row heights are intentionally explicit so the math stays predictable.
Included demo
- Vanilla HTML, CSS, and JavaScript with zero external dependencies.
- React equivalent using the same interaction model.
- A Lab route for trying the behavior in isolation.
Integration checklist
Keep state updates separate from presentation, preserve semantic labels, and add persistence or server callbacks at the boundary where your product needs them.