webdevpuneet.com

Infinite Scroll Table — Lazy-Load Rows HTML CSS JS

Infinite Scroll Table · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="ist-wrap">
  <div class="ist-bar"><h3>Transactions</h3><span class="ist-count" id="istCount"></span></div>
  <div class="ist-scroll" id="istScroll">
    <table class="ist-table">
      <thead><tr><th>ID</th><th>Customer</th><th>Date</th><th class="ist-num">Amount</th></tr></thead>
      <tbody id="istBody"></tbody>
    </table>
    <div class="ist-sentinel" id="istSentinel"><span class="ist-spin"></span>Loading more…</div>
    <div class="ist-end" id="istEnd" hidden>You have reached the end.</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

IntersectionObserver sentinel
Loads the next page when a bottom sentinel scrolls into view — no scroll-listener math.
Scoped to the container
The observer's root is the scroll box, with rootMargin to preload before the end.
Guarded paging
A loading flag and end check prevent duplicate or past-the-end fetches.
Offset/limit contract
fetchPage(offset, limit) mirrors a real backend, so swapping in fetch is one line.
Append, not re-render
New rows are appended so existing rows and scroll position are preserved.
Loading + end states
A spinner while fetching, an explicit end message, and the observer disconnects when done.
Sticky header + count
The header pins while scrolling and a loaded/total count shows progress.
Row fade-in & no library
New rows fade in; pure HTML/CSS/JS with zero dependencies.

About this UI Snippet

Infinite Scroll Table — Lazy-Load Pages of Rows with a Sentinel Observer

Screenshot of the Infinite Scroll Table snippet rendered live

An infinite-scroll table loads rows in pages as the user scrolls toward the bottom, instead of fetching thousands at once — the right pattern for long transaction logs, feeds, and search results where loading everything upfront is slow and wasteful. This snippet builds it in plain HTML, CSS, and vanilla JavaScript with an IntersectionObserver sentinel, a sticky header, a loading spinner, and a proper end state — no library.

A sentinel, not a scroll listener

The next page loads when a sentinel element at the bottom of the list scrolls into view. An IntersectionObserver watches that sentinel (scoped to the scroll container via root, with a rootMargin so loading starts a bit before the user hits the very end). This is the modern replacement for listening to scroll and computing scrollTop + clientHeight >= scrollHeight on every frame — the observer fires only when the sentinel actually appears, off the main thread, so it's both simpler and far more efficient.

Guarded, paged loading

loadMore is guarded by a loading flag and a "have we loaded everything?" check, so overlapping triggers (fast scrolling, the observer firing repeatedly) can't fire duplicate fetches or load past the end. Each call fetches the next PAGE rows from the current offset and appends them. This offset/limit paging is exactly the contract a real backend exposes, so swapping the simulated fetchPage for a real fetch('/api/rows?offset=…&limit=…') is a one-line change.

Appending, not re-rendering

New rows are appended with insertAdjacentHTML('beforeend', …) rather than rebuilding the whole table, so already-rendered rows (and the user's scroll position) are untouched as the list grows. A subtle fade-in animation on new rows signals that more arrived. This append-only approach is what keeps infinite scroll smooth as the dataset reaches hundreds of rows.

Loading and end states

While a page is in flight, a spinner sentinel reads "Loading more…"; when the last page arrives, the sentinel is hidden, an explicit "You have reached the end" message shows, and the observer is disconnected so it stops watching. Telling the user they've reached the end (and stopping the machinery) is the finishing touch that a bare infinite scroll often forgets, leaving a spinner that never resolves.

A sticky header and a count

The header stays pinned with position: sticky as rows scroll beneath it, and a "loaded of total" count gives a sense of progress. The whole thing is a drop-in, dependency-free reference for the sentinel-observer infinite-scroll pattern, applicable to tables, feeds, or any long list. One trade-off worth knowing: because every loaded row stays in the DOM forever, a list that grows into the thousands will eventually slow down scrolling and memory use — past a few hundred rows it's worth combining this sentinel-loading pattern with row virtualization (rendering only the rows currently in or near the viewport) rather than relying on lazy-loading alone.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA transactions table renders its first page and shows a "Loading more…" sentinel.
  2. 2
    Scroll downAs you near the bottom, the next page of rows loads and appends automatically.
  3. 3
    Watch the countThe header shows "loaded of total" so progress is visible.
  4. 4
    Reach the endAfter the last page, an end message shows and loading stops.
  5. 5
    Wire to your APIReplace fetchPage with a real fetch('/api/rows?offset=…&limit=…') call.
  6. 6
    Tune pagingAdjust PAGE size and the observer rootMargin for earlier or later loading.

Real-world uses

Common Use Cases

Transaction and order logs
Lazy-load long financial lists in a data table, fetching the next page when a bottom sentinel scrolls into view.
Activity streams
Page through events with an activity feed, using a loading flag so duplicate fetches never overlap.
Search result tables
Load more matches on demand beside a filterable table of search results, instead of fetching everything upfront.
Admin record browsers
Browse large datasets without a heavy pagination control, scoping the observer to the scroll box with rootMargin to prefetch early.
Sentinel pattern reference
Study fetchPage(offset, limit), which mirrors a real backend, and compare with the list-based infinite scroll pattern.
Related: Sticky Table Header
See the Sticky Table Header for a related tables pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

A scroll listener fires constantly and forces you to compute scrollTop + clientHeight >= scrollHeight on every event, which is wasteful and runs on the main thread. An IntersectionObserver watching a bottom sentinel fires only when that element actually scrolls into view, off the main thread, and a rootMargin lets you start loading before the user hits the very end. It's simpler, faster, and the modern standard for infinite scroll.

loadMore is guarded by a loading boolean and a loaded >= TOTAL check. While a fetch is in flight, loading is true so repeated observer firings (common during fast scrolling) return early; once all rows are loaded it returns early too and the observer is disconnected. This prevents duplicate fetches and loading past the end — the two classic infinite-scroll bugs.

Replace the simulated fetchPage(offset, limit) — which resolves after a timeout — with a real call like fetch('/api/rows?offset=' + offset + '&limit=' + limit).then(r => r.json()). The rest of the code already speaks the offset/limit paging contract that REST APIs expose, appends whatever rows come back, and stops when fewer than a full page (or the known total) is returned.

Rebuilding the whole tbody on each page would discard and recreate already-visible rows, risk losing the scroll position, and get slower as the list grows. insertAdjacentHTML('beforeend', …) appends only the new rows, leaving existing DOM and scroll untouched — which keeps infinite scroll smooth even after hundreds of rows. A fade-in animation marks the newly added rows.

Hold the rows and a loading flag in state, render the tbody from the array, and set up the IntersectionObserver in a useEffect (React), onMounted/onUnmounted (Vue), or ngAfterViewInit/ngOnDestroy (Angular) on a ref to the sentinel, disconnecting on cleanup. On intersect, fetch the next page and append to state. The observer + paging logic is framework-agnostic.