webdevpuneet.com

Virtualized Table — Windowed Row Rendering HTML CSS JS

Virtualized Table (Windowed Rendering) · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="vrt-card">
  <div class="vrt-head">
    <h3>5,000 rows</h3>
    <p class="vrt-hint" id="vrtRange">Rendering rows 0–24 of 5000</p>
  </div>
  <div class="vrt-scroll" id="vrtScroll">
    <div class="vrt-spacer" id="vrtSpacer">
      <table class="vrt-table" id="vrtTable" style="top:0">
        <thead><tr><th>#</th><th>User</th><th>Score</th><th>Region</th><th>Status</th></tr></thead>
        <tbody id="vrtBody"></tbody>
      </table>
    </div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Real windowed rendering
Only visible rows plus a buffer exist in the DOM at any time — never all 5,000.
Full-height spacer element
A sized spacer keeps the scrollbar proportional to the true dataset length.
Live scroll-to-index math
scrollTop is converted to a row index on every scroll event, not a static offset.
Absolute-positioned row window
The rendered table is translated to its true pixel offset so rows land exactly where the full data would place them.
Buffered overscan
Extra rows beyond each viewport edge prevent blank flashes during fast scrolling.
Sticky header
Column labels stay visible within the scroll container regardless of scroll position.
Resize-aware
The visible row count recalculates from the container's live height on window resize.
Live DOM row count readout
A label reports exactly which indices and how many real rows are rendered, for verifying the technique works.

About this UI Snippet

Virtualized Table — Real Windowed Rendering for Thousands of Rows

Screenshot of the Virtualized Table (Windowed Rendering) snippet rendered live

Rendering five thousand <tr> elements into the DOM at once is slow to build, slow to lay out, and heavy on memory — most of those rows are never on screen at any given moment. Virtualization (also called windowing) fixes this by keeping only the rows that are actually visible, plus a small buffer, in the DOM at all times, while a spacer element fakes the full scrollable height so the scrollbar still behaves correctly. This snippet implements genuine windowed rendering over 5,000 generated rows in plain HTML, CSS, and vanilla JavaScript — no library, no shortcuts.

A full-height spacer, a tiny real table

The scroll container holds a .vrt-spacer sized to TOTAL_ROWS * ROW_HEIGHT pixels — the height the table *would* be if every row were rendered — so the browser's native scrollbar reflects the true scrollable distance. Inside it, the actual <table> is absolutely positioned and, at any moment, contains only a couple dozen <tr>s. The spacer is what makes the scrollbar honest while the DOM stays cheap.

Real scroll-position math, not a fixed slice

On every scroll event, renderWindow() reads scroller.scrollTop and converts that pixel offset into a row index with Math.floor((scrollTop - HEADER_HEIGHT) / ROW_HEIGHT) — genuine arithmetic tying the scroll position to which row is first visible, not a hardcoded window that never moves. visibleCount is derived from the viewport's actual clientHeight divided by the fixed row height, so the number of rows rendered adapts if the container is resized.

A buffer so fast scrolling doesn't flash blank rows

Rendering exactly the visible rows and nothing else would show a flicker of empty space during a fast scroll, since the new rows for the next frame haven't rendered yet. BUFFER pads startIndex and endIndex by a few extra rows on each side, so there's always a small overrun of already-rendered content beyond the viewport edge to scroll into before the next renderWindow() call catches up.

Absolute positioning to land rows at their true offset

Because only a slice of the data is ever in the DOM, the rendered <tbody> can't simply start at the top of the container — row 3,000 needs to visually sit 3,000 row-heights down. The <table>'s top style is set to startIndex * ROW_HEIGHT on every render, placing the small set of real rows at the exact pixel offset the full dataset would have put them, so scrolling feels seamless even though the underlying DOM content is constantly being swapped out.

Sticky header, live row count

The header stays position: sticky within the scroll container so column labels remain visible regardless of scroll position, and a live label reports exactly which row indices and how many actual DOM rows are rendered at any moment — useful for seeing the technique work, and a good sanity check that it never renders anywhere close to all 5,000 rows.

Customizing it

Swap fixed-height rows for variable heights (requires tracking cumulative offsets instead of a constant multiply), add horizontal virtualization for very wide tables, or combine with a frozen columns table for pinned columns on a huge dataset. Pair with an infinite scroll table if the data is paginated from a server instead of generated upfront.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JS5,000 rows are generated in an array and the first window renders inside a fixed-height scroll container.
  2. 2
    Scroll the tableOnly the visible rows plus a small buffer exist in the DOM at any moment — check the live row-range label.
  3. 3
    Scroll to the very bottomThe scrollbar reaches its true end because the spacer element reflects the full 5,000-row height.
  4. 4
    Scroll quicklyThe buffer of extra rows above and below the viewport prevents blank flashes during fast scrolling.
  5. 5
    Resize the windowvisibleCount recalculates from the container's live clientHeight, so the rendered window adapts.
  6. 6
    Swap in your own dataReplace DATA and TOTAL_ROWS — the windowing math works for any row count.

Real-world uses

Common Use Cases

Large admin data grids
Browse tens of thousands of records smoothly, rendering only the visible rows plus a buffer using real scrollTop to row index maths.
Log and event viewers
Scroll through large log datasets without freezing the page, with a full-height spacer keeping the scrollbar proportional to the total.
Analytics and reporting tables
Pair with a multi-column sort table so very long reports can be reordered, with the rendered window translated to its true pixel offset.
Financial transaction ledgers
Render thousands of transactions at once, with only about twenty rows of real DOM at any moment instead of five thousand tr elements.
Search results and virtualisation learning
Show a large unpaginated result set, or study a clear, dependency-free example of windowed rendering with constant row height.

Got questions?

Frequently Asked Questions

The browser computes scrollbar size and thumb position from the scrollable content's actual height. Since only a tiny slice of rows is ever rendered, without a spacer sized to the full virtual height (TOTAL_ROWS * ROW_HEIGHT) the scrollbar would reflect only the few rendered rows, making it impossible to scroll to the middle or end of the real dataset.

Without it, the rendered window would exactly match the visible viewport, so a fast scroll could outrun the scroll event handler and briefly show blank space before renderWindow catches up. Padding startIndex and endIndex by a few rows on each side keeps a small overrun of already-rendered rows just out of view, ready to scroll into immediately.

Precompute a cumulative offset array where offsets[i] is the sum of all row heights before row i, then binary-search that array for the row index whose offset is closest to scrollTop instead of using a constant division. It is more work than fixed-height virtualization but the windowing logic (buffer, absolute positioning) stays conceptually the same.

Infinite scroll typically loads more data from a server as the user nears the bottom and keeps appending it to a growing DOM, so the DOM size still increases over time. This virtualized table already has all the data in memory and instead swaps which small slice is rendered as you scroll — the DOM never grows past a couple dozen rows regardless of how far you scroll or how large the dataset is.

The math is framework-agnostic: keep scrollTop in state, derive startIndex/endIndex with the same formulas in a memoized calculation, and render only DATA.slice(startIndex, endIndex) mapped to row components with the table absolutely positioned via a computed top style. Libraries like react-window or react-virtual implement this exact pattern with additional ergonomics if you want to skip hand-rolling it.