iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
Use an IntersectionObserver sentinel to detect when a reader nears the end of a list, then fetch and append the next page behind an isLoading/hasMore guard. Keep a real <button> as an explicit and keyboard-friendly fallback, and expose stable paginated URLs when search indexing matters. This pattern separates three jobs: detecting proximity, requesting data, and rendering content.
Choose automatic loading, a button, or both
Infinite scrolling and “Load more” are two interfaces for the same paginated data. The best implementation usually supports both: automatic loading improves flow for users who want to continue reading, while a visible button provides control, keyboard access, and a recovery path when observation is unavailable or undesirable.
| Approach | Trigger | Strengths | Costs and precautions |
|---|---|---|---|
| Sentinel observer | A small element intersects the viewport or a scroll container | Few clicks; fetch can begin before the user reaches the end | Requires request guards, lifecycle cleanup, status messaging, and a crawlable URL strategy |
| Load more button | User activates a native button | Clear control and natural mouse, keyboard, and touch behavior | Readers must take an extra action; focus and disabled/loading states still need care |
| Progressive enhancement | Button works first; observer enhances it | Works without JavaScript observation and remains accessible | Needs one shared loading function so the two triggers cannot duplicate requests |
MDN identifies infinite scrolling as an Intersection Observer use case. The API has been available across browsers since March 2019, according to MDN. An observer can watch the viewport (the default) or a designated scrolling element.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →A complete, guarded implementation
The following example loads JSON pages from /api/articles?page=1. Each response is expected to contain items and either nextPage (a number or null). Adapt the URL and rendering function to your API.
#1 Best Overall
<main>
<section id="results" aria-live="polite"></section>
<p id="status" role="status"></p>
<button id="loadMore" type="button">Load more</button>
<div id="sentinel" aria-hidden="true"></div>
</main>
<script>
const results = document.querySelector('#results');
const status = document.querySelector('#status');
const button = document.querySelector('#loadMore');
const sentinel = document.querySelector('#sentinel');
let nextPage = 1;
let isLoading = false;
let hasMore = true;
function renderItem(item) {
const article = document.createElement('article');
article.className = 'result';
article.innerHTML = `
<h2><a href="${encodeURI(item.url)}">${escapeHtml(item.title)}</a></h2>
<p>${escapeHtml(item.summary || '')}</p>`;
return article;
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, character => ({
'&': '&', '<': '<', '>': '>',
'"': '"', "'": '''
}[character]));
}
async function loadNextPage() {
if (isLoading || !hasMore) return;
isLoading = true;
button.disabled = true;
button.textContent = 'Loading…';
status.textContent = 'Loading more results';
const requestedPage = nextPage;
try {
const response = await fetch(`/api/articles?page=${requestedPage}`, {
headers: { Accept: 'application/json' }
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
const fragment = document.createDocumentFragment();
for (const item of data.items || []) fragment.append(renderItem(item));
results.append(fragment);
nextPage = data.nextPage ?? null;
hasMore = nextPage !== null && nextPage !== undefined;
if (!hasMore) {
button.hidden = true;
observer.disconnect();
status.textContent = 'All results are loaded.';
} else {
button.hidden = false;
button.textContent = 'Load more';
status.textContent = '';
}
} catch (error) {
console.error(error);
status.textContent = 'Could not load more results. Try again.';
button.hidden = false;
button.textContent = 'Try again';
} finally {
isLoading = false;
button.disabled = false;
}
}
const observer = new IntersectionObserver((entries) => {
if (entries.some(entry => entry.isIntersecting)) loadNextPage();
}, {
root: null,
rootMargin: '0px 0px 400px 0px',
threshold: 0
});
button.addEventListener('click', loadNextPage);
observer.observe(sentinel);
loadNextPage();
</script>
The sentinel sits after the current content. A positive bottom rootMargin starts the request before the sentinel is actually visible, giving the network time to respond. This is a tuning choice: increase it for slow responses, but do not fetch pages users are unlikely to reach.
Why the request guard matters
Observer callbacks can run more than once while a request is in flight. isLoading prevents overlapping requests; hasMore prevents calls after the final page. Advance nextPage only after a successful response. If your API uses cursors, replace the page number with the returned cursor and do not manufacture a cursor on failure.
Render safely and preserve order
Append a document fragment to minimize repeated layout work, and escape text unless your API returns trusted, sanitized HTML. Append new records at the end rather than inserting in the middle. Keep links and headings in a logical order so keyboard and assistive-technology users encounter content predictably.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsUsing a nested scroll container
If the list scrolls inside a panel instead of the window, place the sentinel inside that panel and pass the panel as root:
const panel = document.querySelector('#resultsPanel');
const observer = new IntersectionObserver(loadWhenVisible, {
root: panel,
rootMargin: '0px 0px 200px 0px',
threshold: 0
});
Without root: panel, intersections are calculated against the viewport. The sentinel must be a descendant of the chosen root.
Rank #2
Button, focus, and status accessibility
Use <button type="button">, not a clickable <div>. web.dev explains that native buttons provide built-in mouse and keyboard behavior. Disable the button only while the request is active, expose a short status through role="status" or aria-live, and do not move focus when an automatic load completes. When a user activates the button, leave focus on that button; changing its label to “Try again” lets the same control recover from a failure.
An article stream may qualify for the ARIA feed pattern, but MDN’s feed guidance includes keyboard navigation and positional metadata requirements. Do not add role="feed" to an ordinary list as a styling shortcut. For most result lists, semantic headings, links, a status message, and a real button are simpler and safer.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchCleanup, cancellation, and component lifecycles
When no next page exists, call observer.disconnect() (or unobserve(sentinel)) so the observer no longer schedules callbacks. In a single-page application, disconnect during component teardown as well. You can also cancel a request when a component is removed:
const controller = new AbortController();
fetch('/api/articles?page=2', { signal: controller.signal });
// During teardown:
controller.abort();
observer.disconnect();
Treat an abort as a nonfatal cancellation rather than displaying an error to a page the user has already left. If the server may return duplicate records, deduplicate by a stable item ID before appending.
Search indexing and shareable pages
Do not make important content reachable only by scrolling or clicking. Google Search says its crawler does not scroll or click to trigger content and recommends persistent, unique URLs for chunks of an infinite-scroll page. Keep conventional routes such as /articles?page=2 (or stable cursor URLs) that return the corresponding items server-side or through a crawlable link. JavaScript can enhance that route into one continuous list, while each chunk remains addressable and shareable.
Use absolute page numbers when the collection is ordered and stable; use opaque cursors when the API requires them. Update the URL with the History API only when that behavior matches your product, and ensure a reload of the URL still renders the same chunk. A canonical paginated experience can coexist with an enhanced scroller.
Do not confuse data loading with lazy-loaded assets
Fetching the next JSON page is different from deferring images or iframes already present in the DOM. For assets, use native attributes such as <img loading="lazy"> and <iframe loading="lazy"> where appropriate. MDN’s lazy-loading guide covers those resource strategies and event-handler fallbacks for compatibility-sensitive cases. Your observer can trigger a data request; it does not replace native asset loading.
Troubleshooting common failures
Requests fire repeatedly
Check that the callback calls one shared function and that it begins with if (isLoading || !hasMore) return. Do not increment the page before a successful response.
The observer never fires
Confirm the sentinel is inside the scroll root, the root has a usable size, and content is tall enough to create a scroll position. For a panel, set root to that panel. Temporarily add a visible height and background to the sentinel while debugging.
The list stops after an error
Leave hasMore unchanged on failure, reset isLoading in finally, and keep the button available with a retry label. Log the HTTP status, but show users an actionable message.
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #4
Items appear twice
Check whether both the initial call and observer can start at once; the guard should serialize them. Also verify that the server’s cursor or page contract is not returning overlapping pages. Deduplicate by ID as a final defense.
Search engines miss later content
Publish crawlable, stable paginated URLs and links. Do not rely on a crawler executing a scroll gesture. Test each URL directly without JavaScript enhancement.
Performance and reliability checklist
- Request only one page at a time and use a server-side page or cursor limit.
- Choose a modest positive
rootMarginbased on response latency; avoid prefetching many unused pages. - Append a
DocumentFragment, reserve image dimensions to reduce layout shifts, and use native lazy loading for offscreen media. - Return an explicit end-of-list signal such as
nextPage: null; do not infer completion from an empty page unless that is your documented API contract. - Keep a visible retry button and report recoverable failures without trapping focus.
- Disconnect observers when finished or unmounted.
- Measure real requests, error rates, and memory growth in your application rather than assuming infinite scrolling is faster.
Or skip the browser setup
If your goal is to capture a page after its JavaScript has loaded more content, ScreenshotNeo provides a website screenshot API and MCP server. It can wait for a selector, delay, or network idle, click an element before capture, load lazy images, and capture a full page. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
One GET request is enough (see the ScreenshotNeo documentation):
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Equivalent Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Equivalent Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Every feature is on every plan; the Free plan includes 1,000 shots per month with no card, and paid plans start at $5 for 3,000 shots. Sign up for the free 1,000-shot plan.
FAQ
Should I use a page number or a cursor?
Use page numbers when stable, addressable chunks matter for navigation and indexing. Use the cursor your API returns when records can change between requests or the backend requires opaque continuation state.
Best Value
Can I observe the button instead of a separate sentinel?
Yes, but a dedicated sentinel keeps the control’s visibility and semantics independent. If the button is observed, ensure it remains present and that loading or hiding it does not prevent the final callback you need.
Is IntersectionObserver required for infinite scrolling?
No. A button-only implementation is valid, and a scroll-event fallback can support unusual compatibility constraints. IntersectionObserver avoids continuous scroll calculations and is broadly available.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →How do I prevent an enormous page?
Offer a conventional paginated route, cap the number of automatically appended pages, or replace older items with a deliberate virtualized-list design. Do not remove content that a keyboard or assistive-technology user is currently navigating without a clear, documented interaction.
Frequently Asked Questions
Should I use a page number or a cursor?
Use page numbers for stable, addressable chunks; use the API’s cursor when records can change between requests or the backend requires opaque continuation state.
Can I observe the button instead of a separate sentinel?
Yes, provided the button remains present and your loading and hiding logic cannot remove the target before the needed callback.
Is IntersectionObserver required for infinite scrolling?
No. A button-only interface is valid; IntersectionObserver is a convenient broadly available enhancement.
How do I prevent an enormous page?
Cap automatic appends, retain conventional pagination, or adopt a deliberate virtualized-list design with accessible focus behavior.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

