Infinite Scroll (No Framework)

January 18, 2026

A lightweight, drop-in infinite scroll script that loads your next page automatically when the user scrolls near the bottom. No frameworks, no build steps.

What it does:

  • Watches a “sentinel” element at the bottom of your list.
  • When it becomes visible, fetches the next page and appends new items.
  • Keeps URLs sane (optional): updates the address bar as pages load.
  • Graceful fallback: if JS is off, normal pagination still works.

Install (newbie-friendly):

  1. Make sure your page already has working pagination links (Page 2, Page 3...).
  2. Wrap your items in a container with an id (example: id="feed").
  3. Add a “next page” link with a predictable selector (example: a rel="next").
  4. Paste the script below near the bottom of your page (before </body>).

HTML structure example:

  • Your items: <div id="feed">...items...</div>
  • Your next link: <a rel="next" href="/?page=2">Next</a>

Tip: This script fetches the next page, extracts just the items from #feed, and appends them. So your server can stay “boring” and keep using normal pagination.

<script>
(function(){
  /**
   * Tiny Infinite Scroll (No Framework)
   *
   * Requirements on your page:
   *   - Items live inside:   <div id="feed"> ... </div>
   *   - Next page link:      <a rel="next" href="/?page=2">Next</a>
   *
   * Optional:
   *   - Add data-append="true" to allow multiple lists (not needed here)
   */

  var FEED_SEL = "#feed";
  var NEXT_SEL = 'a[rel="next"]';
  var SENTINEL_ID = "infinite-sentinel";
  var LOADING_ID = "infinite-loading";

  var feed = document.querySelector(FEED_SEL);
  if (!feed) return;

  function getNextHref(){
    var a = document.querySelector(NEXT_SEL);
    return a ? a.getAttribute("href") : "";
  }

  function setNextHref(newHref){
    var a = document.querySelector(NEXT_SEL);
    if (!a) return;
    if (!newHref) {
      // no more pages: hide next link
      a.style.display = "none";
      return;
    }
    a.setAttribute("href", newHref);
  }

  function ensureSentinel(){
    var s = document.getElementById(SENTINEL_ID);
    if (s) return s;
    s = document.createElement("div");
    s.id = SENTINEL_ID;
    s.style.height = "1px";
    s.style.marginTop = "12px";
    feed.after(s);
    return s;
  }

  function ensureLoading(){
    var l = document.getElementById(LOADING_ID);
    if (l) return l;
    l = document.createElement("div");
    l.id = LOADING_ID;
    l.style.margin = "14px 0";
    l.style.font = "700 13px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif";
    l.style.opacity = "0.75";
    l.style.display = "none";
    l.textContent = "Loading more…";
    feed.after(l);
    return l;
  }

  var sentinel = ensureSentinel();
  var loading  = ensureLoading();

  var busy = false;
  var loadedPages = 1;

  function sameOrigin(url){
    try{
      var u = new URL(url, location.href);
      return u.origin === location.origin;
    }catch(e){
      return false;
    }
  }

  function fetchNext(){
    if (busy) return;
    var href = getNextHref();
    if (!href) return;

    // Safety: only fetch same-origin pages
    if (!sameOrigin(href)) return;

    busy = true;
    loading.style.display = "block";

    fetch(href, { credentials: "same-origin" })
      .then(function(r){
        if (!r.ok) throw new Error("HTTP " + r.status);
        return r.text();
      })
      .then(function(html){
        var doc = new DOMParser().parseFromString(html, "text/html");

        var newFeed = doc.querySelector(FEED_SEL);
        if (!newFeed) throw new Error("Missing feed on next page.");

        // Append children
        var frag = document.createDocumentFragment();
        while (newFeed.firstElementChild) {
          frag.appendChild(newFeed.firstElementChild);
        }
        feed.appendChild(frag);

        // Update next link from the fetched page
        var newNext = doc.querySelector(NEXT_SEL);
        var newHref = newNext ? newNext.getAttribute("href") : "";
        setNextHref(newHref);

        loadedPages++;

        // Optional: update URL as you load more (keeps share/refresh sane)
        // NOTE: This keeps it simple: updates to the latest loaded page href.
        try {
          var u = new URL(href, location.href);
          history.replaceState({page: loadedPages}, "", u.pathname + u.search + u.hash);
        } catch(e) {}

      })
      .catch(function(){
        // If something fails, stop trying and keep normal pagination visible
      })
      .finally(function(){
        busy = false;
        loading.style.display = "none";
      });
  }

  // Prefer IntersectionObserver, fallback to scroll
  if ("IntersectionObserver" in window) {
    var io = new IntersectionObserver(function(entries){
      if (!entries || !entries[0]) return;
      if (entries[0].isIntersecting) fetchNext();
    }, { root: null, rootMargin: "600px 0px", threshold: 0.01 });
    io.observe(sentinel);
  } else {
    window.addEventListener("scroll", function(){
      var rect = sentinel.getBoundingClientRect();
      if (rect.top < window.innerHeight + 600) fetchNext();
    }, { passive:true });
  }
})();
</script>

Comments (0)

No comments yet — be the first.

← Back to all scripts