Infinite Scroll (No Framework)
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):
- Make sure your page already has working pagination links (Page 2, Page 3...).
- Wrap your items in a container with an id (example:
id="feed"). - Add a “next page” link with a predictable selector (example:
a rel="next"). - 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.