Virtual List

Efficiently rendered large list.

Contacts (2,400 rows)

Only the rows in view are kept in the DOM; the scrollbar tracks the full list.

  • Alicia Ferreira
  • Ben Sato
  • Charu Deshpande
  • Diego Morales
  • Erin Walsh
  • Femi Adeyemi
  • Grace Kim

9 of 2,400 rows exist in the DOM right now.

html

<div class="vl" style="--accent:#D9C8F0;">
  <h3 class="vl__heading">Contacts (2,400 rows)</h3>
  <p class="vl__explain">Only the rows in view are kept in the DOM; the scrollbar tracks the full list.</p>
  <div class="vl__viewport">
    <ul class="vl__list" data-window="5">
      <li style="height:34px;">Alicia Ferreira</li>
      <li style="height:34px;">Ben Sato</li>
      <li style="height:34px;">Charu Deshpande</li>
      <li style="height:34px;">Diego Morales</li>
      <li style="height:34px;">Erin Walsh</li>
      <li style="height:34px;">Femi Adeyemi</li>
      <li style="height:34px;">Grace Kim</li>
    </ul>
    <div class="vl__spacer" aria-hidden="true"></div>
  </div>
  <p class="vl__status" role="status">9 of 2,400 rows exist in the DOM right now.</p>
</div>
<style>
.vl { font: 14px/1.4 system-ui, sans-serif; color: #241430; background: #FAF7F2; max-width: 300px; width: 300px; padding: 16px; border-radius: 12px; box-sizing: border-box; }
.vl__heading { margin: 0 0 4px; font-size: 15px; font-weight: 700; }
.vl__explain { margin: 0 0 10px; font-size: 12px; color: #241430a0; }
.vl__viewport { max-height: 176px; overflow: auto; position: relative; border: 1px solid #24143020; border-radius: 8px; }
.vl__list { list-style: none; margin: 0; padding: 0; }
.vl__list li { display: flex; align-items: center; padding: 0 10px; border-bottom: 1px solid #24143014; border-left: 3px solid var(--accent); background: #fff; }
ul[data-window="3"] li:nth-child(n+4) { display: none; }
ul[data-window="5"] li:nth-child(n+6) { display: none; }
ul[data-window="7"] li:nth-child(n+8) { display: none; }
.vl__spacer { height: 900px; background: repeating-linear-gradient(180deg, #24143008 0 1px, transparent 1px 34px); }
.vl__status { margin: 10px 0 0; font-size: 12px; color: #241430a0; }
</style>

tailwind

<div class="vl-tw font-sans text-sm text-[#241430] bg-[#FAF7F2] max-w-[300px] w-[300px] p-4 rounded-xl box-border" style="--accent:#D9C8F0">
  <h3 class="m-0 mb-1 text-[15px] font-bold">Contacts (2,400 rows)</h3>
  <p class="m-0 mb-2.5 text-xs text-[#241430a0]">Only the rows in view are kept in the DOM; the scrollbar tracks the full list.</p>
  <div class="vl-tw__viewport max-h-[176px] overflow-auto relative border border-[#24143020] rounded-lg">
    <ul class="vl-tw__list list-none m-0 p-0" data-window="5">
      <li class="flex items-center px-2.5 border-b border-[#24143014] bg-white" style="height:34px;border-left:3px solid #D9C8F0">Alicia Ferreira</li>
      <li class="flex items-center px-2.5 border-b border-[#24143014] bg-white" style="height:34px;border-left:3px solid #D9C8F0">Ben Sato</li>
      <li class="flex items-center px-2.5 border-b border-[#24143014] bg-white" style="height:34px;border-left:3px solid #D9C8F0">Charu Deshpande</li>
      <li class="flex items-center px-2.5 border-b border-[#24143014] bg-white" style="height:34px;border-left:3px solid #D9C8F0">Diego Morales</li>
      <li class="flex items-center px-2.5 border-b border-[#24143014] bg-white" style="height:34px;border-left:3px solid #D9C8F0">Erin Walsh</li>
      <li class="flex items-center px-2.5 border-b border-[#24143014] bg-white" style="height:34px;border-left:3px solid #D9C8F0">Femi Adeyemi</li>
      <li class="flex items-center px-2.5 border-b border-[#24143014] bg-white" style="height:34px;border-left:3px solid #D9C8F0">Grace Kim</li>
    </ul>
    <div aria-hidden="true" class="h-[900px]" style="background:repeating-linear-gradient(180deg, #24143008 0 1px, transparent 1px 34px);"></div>
  </div>
  <p role="status" class="mt-2.5 mb-0 text-xs text-[#241430a0]">9 of 2,400 rows exist in the DOM right now.</p>
</div>
<style>
ul[data-window="3"] li:nth-child(n+4) { display: none; }
ul[data-window="5"] li:nth-child(n+6) { display: none; }
ul[data-window="7"] li:nth-child(n+8) { display: none; }
</style>

react

export function VirtualList({
  heading = "Contacts (2,400 rows)",
  accent = "#D9C8F0",
  windowSize = "5",
  rowHeight = 34,
  rendered = 9,
}) {
  const names = ["Alicia Ferreira", "Ben Sato", "Charu Deshpande", "Diego Morales", "Erin Walsh", "Femi Adeyemi", "Grace Kim"].slice(0, Number(windowSize));
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", fontSize: 14, color: "#241430", background: "#FAF7F2", maxWidth: 300, width: 300, padding: 16, borderRadius: 12, boxSizing: "border-box" }}>
      <h3 style={{ margin: "0 0 4px", fontSize: 15, fontWeight: 700 }}>{heading}</h3>
      <p style={{ margin: "0 0 10px", fontSize: 12, color: "#241430a0" }}>Only the rows in view are kept in the DOM; the scrollbar tracks the full list.</p>
      <div style={{ maxHeight: 176, overflow: "auto", position: "relative", border: "1px solid #24143020", borderRadius: 8 }}>
        <ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
          {names.map((name) => (
            <li key={name} style={{ display: "flex", alignItems: "center", height: rowHeight, padding: "0 10px", borderBottom: "1px solid #24143014", borderLeft: `3px solid ${accent}`, background: "#fff" }}>{name}</li>
          ))}
        </ul>
        <div aria-hidden="true" style={{ height: 900, background: "repeating-linear-gradient(180deg, #24143008 0 1px, transparent 1px 34px)" }} />
      </div>
      <p role="status" style={{ margin: "10px 0 0", fontSize: 12, color: "#241430a0" }}>{rendered} of 2,400 rows exist in the DOM right now.</p>
    </div>
  );
}