webdevpuneet.com

Virtualized 100k-Row Table — Free JS Snippet

Virtualized 100,000-Row Table in Vanilla JavaScript · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="vt-wrap">
  <div class="vt-bar">
    <input id="vtSearch" type="search" placeholder="Filter by name or email..." aria-label="Filter rows">
    <label class="vt-jump">Jump to row <input id="vtJump" type="number" min="1" value="50000"></label>
    <button type="button" id="vtGo">Go</button>
  </div>
  <div class="vt-stats" aria-live="polite">
    <span><b id="vtTotal">100,000</b> rows in data</span>
    <span><b id="vtShown">0</b> shown</span>
    <span class="vt-hot"><b id="vtDom">0</b> DOM rows</span>
  </div>
  <div class="vt-table" role="table" aria-label="Virtualized data table" aria-rowcount="100000">
    <div class="vt-head" role="row" id="vtHead"></div>
    <div class="vt-scroll" id="vtScroll" tabindex="0">
      <div class="vt-sizer" id="vtSizer"><div class="vt-rows" id="vtRows"></div></div>
    </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

Renders only visible rows plus a small buffer — roughly 20 of 100,000
Fixed row height makes the visible range simple arithmetic
Scrollbar height from a sizer element and a translateY-positioned row window
requestAnimationFrame coalescing of scroll events
Sort and filter via an index view array — rows are never copied
Sort indicators with aria-sort and true aria-rowcount / aria-rowindex
Debounced filter and a jump-to-row control that centres and flashes the row
No library, no build step

About this UI Snippet

Virtualized 100,000-Row Table — HTML, CSS & JavaScript

Screenshot of the Virtualized 100,000-Row Table in Vanilla JavaScript snippet rendered live

Put a hundred thousand rows into a normal HTML table and the browser stops being usable. Creating that many elements takes seconds, every one of them costs memory, and layout and scrolling turn into a slideshow. The fix is virtualization, sometimes called windowing: the user can only see about ten rows at once, so render only those and fake the rest. The scrollbar still represents the whole dataset, but the DOM holds a few dozen rows at any moment. The counters above the table make this concrete — 100,000 rows in the data, but only around 20 in the DOM.

The technique is surprisingly small when row height is fixed, which is why this snippet fixes it at 36 pixels. An empty sizer element is given a height of rowCount × 36, which makes the browser draw a scrollbar for the full dataset. On every scroll, the first visible row is scrollTop divided by row height and the last is that plus the viewport height divided by row height — plain arithmetic, no measuring of any element. Only that slice is turned into HTML, with a few buffer rows above and below so fast scrolling never flashes blank space, and the whole rows container is moved into place with a translateY transform. Variable row heights are much harder because you must measure or estimate every row; fixed height is the technique's sweet spot.

Two performance details matter as much as the windowing. Scroll events fire far more often than frames are painted, so the handler only schedules a render with requestAnimationFrame if one is not already pending, coalescing bursts into one update per frame. And the data is never copied for sorting or filtering: a view array holds indexes into the original rows, and is null when nothing is applied so no hundred-thousand-entry array is allocated. Sorting sorts the indexes, not the objects.

The rest is what makes it a usable table. Header buttons sort with aria-sort updated for assistive technology, the filter is debounced by 150 milliseconds, and Jump to row sets scrollTop so the target row lands in the middle and briefly flashes. Because only visible rows exist, browser find-in-page and screen-reader table navigation cannot reach off-screen rows, which is the real cost of virtualization; aria-rowcount and aria-rowindex tell assistive technology the true size and position, and a server-side search is the honest answer when full-text find matters.

Step by step

How to Use

  1. 1
    Scroll fastDrag the scrollbar or use the mouse wheel. The DOM row counter stays near 20 while the table represents 100,000 rows.
  2. 2
    Sort a columnClick a header to sort all 100,000 rows. Click again to reverse. The sort arrow follows the active column.
  3. 3
    FilterType a name or email fragment. The row count drops and the scrollbar shrinks to match the results.
  4. 4
    Jump to a rowEnter a row number and press Go. The table scrolls there, centres the row and flashes it.
  5. 5
    Watch the countersCompare "rows in data", "shown" and "DOM rows" to see how few elements are actually rendered.

Real-world uses

Common Use Cases

📜
Logs and audit trails
Browse very long event lists, rendering only about 20 of 100,000 rows with a small buffer so scrolling stays smooth.
🗂️
Large exports and admin lists
Show every customer or order without pagination, using fixed row height so the visible range is simple arithmetic.
🧰
Developer data browsers
Build data browsers and inspectors that must stay fast with huge datasets, with scroll events coalesced through requestAnimationFrame.
⚖️
Library comparison
Compare with the Tabulator sortable filterable grid for a library-based approach to very large tables with built-in features.
🎓
Windowing learning
Study a dependency-free explanation of how a sizer element creates the scrollbar height while a translated row window shows only the visible slice.

Got questions?

Frequently Asked Questions

Rendering only the items currently visible, plus a small buffer, while making the scroll area the size of the full list. The DOM stays small no matter how much data there is.

With a constant height, the first visible row is just scrollTop / rowHeight. Variable heights require measuring or estimating every row.

Scroll events can fire many times per frame. Scheduling one render per animation frame avoids wasted work and jank.

It sorts an array of indexes rather than copying row objects, and only the visible slice is turned into DOM.

Off-screen rows do not exist, so find-in-page and screen reader navigation cannot reach them. aria-rowcount and aria-rowindex describe the full size, but server-side search is better for finding text.

react-window and react-virtual for React, TanStack Virtual for several frameworks, and AG Grid or Tabulator's virtual DOM for full grids.

Yes. Use the JSX, Vue, Angular or Tailwind export buttons on this page. It has no library dependency, so the logic ports directly: keep the scroll position in state, compute the first and last visible index with the same arithmetic, and render only that slice. For production React or Vue apps, TanStack Virtual or react-window give you the same technique with variable row heights.