UI Components Medium
Windowed List
Render only the visible rows in a fixed-height scroll surface with overscan.
Open in Lab
MCP
html css javascript react
Targets: TS JS HTML React
Code
:root {
--bg: #0b1020;
--surface: #121a2d;
--surface-2: #18233b;
--line: #2b3d62;
--text: #eef2ff;
--muted: #9eb1d4;
--accent: #78a9ff;
--row-h: 44px;
}
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
padding: clamp(1rem, 4vw, 3rem);
}
button, input { font: inherit; }
.demo {
width: min(760px, 100%);
margin: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 20px;
padding: clamp(1rem, 3vw, 1.75rem);
box-shadow: 0 20px 70px #0006;
}
.panel-head {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: flex-start;
justify-content: space-between;
}
h1 { margin: 0; font-size: 1.1rem; letter-spacing: -0.01em; }
.sub { margin: 0.25rem 0 0; color: var(--muted); font-size: 0.85rem; }
.stats { display: flex; gap: 1rem; margin: 0; }
.stats div { text-align: right; }
.stats dt {
color: var(--muted);
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.stats dd {
margin: 2px 0 0;
font-variant-numeric: tabular-nums;
font-size: 0.95rem;
color: var(--accent);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.65rem;
margin: 1.1rem 0 0.85rem;
font-size: 0.82rem;
color: var(--muted);
}
.controls input[type="range"] { accent-color: var(--accent); flex: 1 1 130px; }
.controls input[type="number"] {
width: 88px;
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--text);
border-radius: 8px;
padding: 0.35rem 0.5rem;
}
.controls button {
background: var(--accent);
color: #08111f;
border: 0;
border-radius: 8px;
padding: 0.4rem 0.9rem;
font-weight: 600;
cursor: pointer;
}
.controls button:hover { filter: brightness(1.1); }
output { color: var(--accent); font-variant-numeric: tabular-nums; }
.viewport {
height: 396px;
overflow-y: auto;
overscroll-behavior: contain;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 14px;
}
.viewport:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.spacer { position: relative; width: 100%; }
.rows {
list-style: none;
margin: 0;
padding: 0;
position: absolute;
top: 0;
left: 0;
right: 0;
will-change: transform;
}
.row {
height: var(--row-h);
display: grid;
grid-template-columns: 58px 1fr auto auto;
align-items: center;
gap: 0.75rem;
padding: 0 0.9rem;
border-bottom: 1px solid #ffffff0a;
font-size: 0.86rem;
}
.row[hidden] {
display: none;
}
.row[aria-selected="true"] {
background: #78a9ff24;
box-shadow: inset 3px 0 0 var(--accent);
}
.idx { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 0.75rem; }
.name { display: flex; align-items: center; gap: 0.55rem; min-width: 0; }
.name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: #5ad19a; flex: none; }
.dot[data-state="warn"] { background: #f2c14e; }
.dot[data-state="down"] { background: #f2665e; }
.val { font-variant-numeric: tabular-nums; color: var(--muted); font-size: 0.8rem; }
.tag {
font-size: 0.68rem;
padding: 2px 0.5rem;
border-radius: 99px;
background: #ffffff10;
color: var(--muted);
}
.hint { margin: 0.85rem 0 0; color: var(--muted); font-size: 0.8rem; }
@media (prefers-reduced-motion: reduce) {
.viewport { scroll-behavior: auto; }
* { transition: none !important; animation: none !important; }
}
@media (max-width: 600px) {
.demo { border-radius: 16px; }
.row { grid-template-columns: 48px 1fr auto; }
.row .tag { display: none; }
}/* Windowed list: fixed row height, absolute spacer, translated row pool. */
(() => {
const viewport = document.getElementById("viewport");
const spacer = document.getElementById("spacer");
const rowsEl = document.getElementById("rows");
const overscanInput = document.getElementById("overscan");
const overscanOut = document.getElementById("overscan-out");
const jumpInput = document.getElementById("jump");
const jumpBtn = document.getElementById("jump-btn");
const statTotal = document.getElementById("stat-total");
const statDom = document.getElementById("stat-dom");
const statRange = document.getElementById("stat-range");
const ROW_H = 44;
const TOTAL = 20000;
const STATES = ["ok", "ok", "ok", "warn", "down"];
const REGIONS = ["us-east", "us-west", "eu-central", "ap-south", "sa-east"];
const NAMES = ["ingest", "auth", "billing", "search", "media", "queue", "edge", "reports"];
// Deterministic pseudo-random so rows are stable across re-renders.
const hash = (n) => {
let x = (n * 2654435761) % 4294967296;
x ^= x >>> 15;
return Math.abs(x);
};
const record = (i) => {
const h = hash(i + 1);
return {
name: `${NAMES[h % NAMES.length]}-${String(i + 1).padStart(5, "0")}`,
state: STATES[(h >> 3) % STATES.length],
ms: 12 + ((h >> 7) % 780),
region: REGIONS[(h >> 11) % REGIONS.length],
};
};
let overscan = Number(overscanInput.value);
let active = 0;
let start = -1;
let end = -1;
let frame = 0;
spacer.style.height = `${TOTAL * ROW_H}px`;
statTotal.textContent = TOTAL.toLocaleString();
const pool = []; // reused <li> elements
function makeRow() {
const li = document.createElement("li");
li.className = "row";
li.setAttribute("role", "option");
li.innerHTML =
'<span class="idx"></span>' +
'<span class="name"><i class="dot"></i><span></span></span>' +
'<span class="val"></span>' +
'<span class="tag"></span>';
rowsEl.append(li);
return li;
}
function paint(li, i) {
const r = record(i);
li.id = `row-${i}`;
li.dataset.index = String(i);
li.setAttribute("aria-posinset", String(i + 1));
li.setAttribute("aria-setsize", String(TOTAL));
li.setAttribute("aria-selected", i === active ? "true" : "false");
li.children[0].textContent = String(i + 1);
li.children[1].firstElementChild.dataset.state = r.state;
li.children[1].lastElementChild.textContent = r.name;
li.children[2].textContent = `${r.ms} ms`;
li.children[3].textContent = r.region;
}
function render(force) {
const scrollTop = viewport.scrollTop;
const visible = Math.ceil(viewport.clientHeight / ROW_H);
const first = Math.max(0, Math.floor(scrollTop / ROW_H) - overscan);
const last = Math.min(TOTAL, first + visible + overscan * 2 + 1);
if (!force && first === start && last === end) return;
start = first;
end = last;
const need = last - first;
while (pool.length < need) pool.push(makeRow());
for (let k = 0; k < pool.length; k++) {
const li = pool[k];
if (k < need) {
li.hidden = false;
paint(li, first + k);
} else if (!li.hidden) {
li.hidden = true;
}
}
rowsEl.style.transform = `translateY(${first * ROW_H}px)`;
statDom.textContent = String(need);
statRange.textContent = `${first + 1}–${last}`;
}
function schedule() {
if (frame) return;
frame = requestAnimationFrame(() => {
frame = 0;
render(false);
});
}
function setActive(i, { scroll = true } = {}) {
active = Math.max(0, Math.min(TOTAL - 1, i));
if (scroll) {
const top = active * 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;
}
}
viewport.setAttribute("aria-activedescendant", `row-${active}`);
render(true);
}
viewport.addEventListener("scroll", schedule, { passive: true });
new ResizeObserver(() => render(true)).observe(viewport);
viewport.addEventListener("keydown", (e) => {
const page = Math.max(1, Math.floor(viewport.clientHeight / ROW_H) - 1);
const moves = {
ArrowDown: active + 1,
ArrowUp: active - 1,
PageDown: active + page,
PageUp: active - page,
Home: 0,
End: TOTAL - 1,
};
if (!(e.key in moves)) return;
e.preventDefault();
setActive(moves[e.key]);
});
rowsEl.addEventListener("click", (e) => {
const li = e.target.closest(".row");
if (li) setActive(Number(li.dataset.index), { scroll: false });
});
overscanInput.addEventListener("input", () => {
overscan = Number(overscanInput.value);
overscanOut.textContent = overscanInput.value;
render(true);
});
const jump = () => {
const i = Math.max(1, Math.min(TOTAL, Number(jumpInput.value) || 1)) - 1;
viewport.scrollTop = i * ROW_H;
setActive(i, { scroll: false });
viewport.focus();
};
jumpBtn.addEventListener("click", jump);
jumpInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") jump();
});
render(true);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Windowed List</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="demo" data-demo="Windowed List">
<header class="panel-head">
<div>
<h1>Windowed List</h1>
<p class="sub">Only the visible rows exist in the DOM.</p>
</div>
<dl class="stats">
<div><dt>Total</dt><dd id="stat-total">0</dd></div>
<div><dt>In DOM</dt><dd id="stat-dom">0</dd></div>
<div><dt>Range</dt><dd id="stat-range">0–0</dd></div>
</dl>
</header>
<div class="controls">
<label for="overscan">Overscan <output id="overscan-out">4</output></label>
<input id="overscan" type="range" min="0" max="20" value="4" step="1" />
<label for="jump">Jump to</label>
<input id="jump" type="number" min="1" max="20000" value="1" />
<button id="jump-btn" type="button">Go</button>
</div>
<div
class="viewport"
id="viewport"
role="listbox"
aria-label="Windowed list of service records"
tabindex="0"
>
<div class="spacer" id="spacer">
<ul class="rows" id="rows"></ul>
</div>
</div>
<p class="hint">Scroll, or focus the list and use Arrow / Page / Home / End keys.</p>
</main>
<script src="script.js"></script>
</body>
</html>import { useMemo, useState } from "react";
export function VirtualScrollWindowedList() {
const [offset, setOffset] = useState(0);
const rows = useMemo(() => Array.from({ length: 20 }, (_, index) => index + offset), [offset]);
return (
<section className="demo">
<h2>Windowed List</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>
);
}Windowed List
Render only the visible rows in a fixed-height scroll surface with overscan.
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.