Infinite scroll loads the next page of posts automatically as the user scrolls to the bottom, replacing traditional pagination. The WordPress REST API makes this straightforward: fetch the next batch of posts with JavaScript and append them to the DOM without a full page reload.
Problem: A blog or archive page uses a "Load More" button that reloads the page on every click, which interrupts reading and does not feel like modern pagination.
Solution: Fetch the next page of posts via the WordPress REST API using the browser's fetch() API, append the returned data to the existing list without a page reload, and update the page counter. Pass a nonce and the REST URL from PHP using wp_localize_script().
The HTML structure — a container and a sentinel element at the bottom:
<div id="posts-container">
<!-- Initial posts rendered by PHP go here -->
</div>
<div id="load-more-sentinel"></div>
<p id="loading-indicator" style="display:none;">Loading…</p>
The JavaScript — using the IntersectionObserver API to detect when the sentinel is visible:
( function() {
let page = 1; // We already have page 1 from PHP
let loading = false;
let maxPages = parseInt( wpInfiniteScroll.totalPages, 10 );
const container = document.getElementById( 'posts-container' );
const sentinel = document.getElementById( 'load-more-sentinel' );
const indicator = document.getElementById( 'loading-indicator' );
async function loadNextPage() {
if ( loading || page >= maxPages ) return;
loading = true;
page++;
indicator.style.display = 'block';
const url = `${wpInfiniteScroll.restUrl}wp/v2/posts`
+ `?per_page=10&page=${page}&_fields=id,title,link,excerpt`;
try {
const res = await fetch( url );
const posts = await res.json();
posts.forEach( post => {
const article = document.createElement( 'article' );
article.innerHTML = `
<h2><a href="${post.link}">${post.title.rendered}</a></h2>
${post.excerpt.rendered}
`;
container.appendChild( article );
} );
} catch ( e ) {
console.error( 'Infinite scroll error:', e );
page--; // allow a retry
} finally {
loading = false;
indicator.style.display = 'none';
}
}
const observer = new IntersectionObserver( entries => {
if ( entries[0].isIntersecting ) loadNextPage();
}, { rootMargin: '200px' } );
observer.observe( sentinel );
} )();
Localize the required data from PHP:
add_action( 'wp_enqueue_scripts', function() {
if ( ! is_home() && ! is_archive() ) return;
wp_enqueue_script( 'infinite-scroll', get_template_directory_uri() . '/js/infinite-scroll.js', [], '1.0', true );
global $wp_query;
wp_localize_script( 'infinite-scroll', 'wpInfiniteScroll', [
'restUrl' => esc_url_raw( rest_url() ),
'totalPages' => (int) $wp_query->max_num_pages,
] );
} );
NOTE: IntersectionObserver is supported in all major browsers as of 2019. For the small percentage of users on older browsers, provide a fallback "Load More" button that calls loadNextPage() on click instead of relying on the observer.