Infinite Scroll in WordPress

Infinite scroll, when used correctly, is a wonderful tool for improving engagement on your website. You lower your users’ barrier to getting the next page of content, which increases their chances to find something interesting and click through.

I refer to infinite scrolling as any technique that uses AJAX to load additional content without reloading the page. I’ll provide a general walkthrough of the tools required, then provide two examples of different implementations.

Summary

There are three main components to infinite scroll.

  1. The trigger. This is what starts the loading of new content. It is typically scroll-based (ex: load more posts when users is X pixels from bottom of page) or click-based (ex: clicking a “load more” button)
  2. The AJAX query. This is the Javascript actually asking for the new content, and then inserting it into the page.
  3. The WordPress query. The PHP code in your theme or plugin that does a WP Query, formats the data, and returns it when asked by the AJAX query.

Scroll to Load More

The first example is a standard “Scroll to Load More”. When the user is a certain distance from the end of the post listing, more posts are loaded and inserted at the bottom. I implemented this on Western Journalism.

infinite scroll on Western Journalism

The WordPress Query

When you create your AJAX query, you’ll specify an action (see load-more.js, line 23). You’ll then hook a WordPress function to that action, and it will get run any time that AJAX query runs.

The hooks you use are wp_ajax_{action} and wp_ajax_nopriv_{action}. Let’s call our action “be_ajax_load_more”, and we’d hook our function like so:

<?php
/**
* AJAX Load More
* @link http://www.billerickson.net/infinite-scroll-in-wordpress
*/
function be_ajax_load_more() {
$data = 'Additional posts go here';
wp_send_json_success( $data );
wp_die();
}
add_action( 'wp_ajax_be_ajax_load_more', 'be_ajax_load_more' );
add_action( 'wp_ajax_nopriv_be_ajax_load_more', 'be_ajax_load_more' );
view raw functions.php hosted with ❤ by GitHub

You’ll pass useful data to your function using the $_POST variable. Here’s a full example of the WordPress query. We’re grabbing query variables from $_POST, doing the query, and returning the result. I’m using a function called be_post_summary() to output the individual post. You’ll need to define this function yourself so it outputs the post formatted the way you’d like.

<?php
/**
* AJAX Load More
* @link http://www.billerickson.net/infinite-scroll-in-wordpress
*/
function be_ajax_load_more() {
$args = isset( $_POST['query'] ) ? array_map( 'esc_attr', $_POST['query'] ) : array();
$args['post_type'] = isset( $args['post_type'] ) ? esc_attr( $args['post_type'] ) : 'post';
$args['paged'] = esc_attr( $_POST['page'] );
$args['post_status'] = 'publish';
ob_start();
$loop = new WP_Query( $args );
if( $loop->have_posts() ): while( $loop->have_posts() ): $loop->the_post();
be_post_summary();
endwhile; endif; wp_reset_postdata();
$data = ob_get_clean();
wp_send_json_success( $data );
wp_die();
}
add_action( 'wp_ajax_be_ajax_load_more', 'be_ajax_load_more' );
add_action( 'wp_ajax_nopriv_be_ajax_load_more', 'be_ajax_load_more' );
view raw functions.php hosted with ❤ by GitHub

Enqueue the Javascript

We need to enqueue a Javascript file which will contain all our relevant JS. We also need to pass along the information our WordPress plugin needs using wp_localize_script().

<?php
/**
* Javascript for Load More
*
*/
function be_load_more_js() {
global $wp_query;
$args = array(
'url' => admin_url( 'admin-ajax.php' ),
'query' => $wp_query->query,
);
wp_enqueue_script( 'be-load-more', get_stylesheet_directory_uri() . '/js/load-more.js', array( 'jquery' ), '1.0', true );
wp_localize_script( 'be-load-more', 'beloadmore', $args );
}
add_action( 'wp_enqueue_scripts', 'be_load_more_js' );
view raw functions.php hosted with ❤ by GitHub

In this example I’m also passing along the URL for the AJAX query and the current WordPress query so that we can use it when requesting additional posts.

Setup the Javascript

Now it’s time for the actual Javascript file. In my theme I created a /js directory and load-more.js inside it. You can name and place your file anywhere, but make sure the wp_enqueue_script() above properly links to it. Below the code snippet I’ll walk through what’s happening line-by-line.

jQuery(function($){
$('.post-listing').append( '<span class="load-more"></span>' );
var button = $('.post-listing .load-more');
var page = 2;
var loading = false;
var scrollHandling = {
allow: true,
reallow: function() {
scrollHandling.allow = true;
},
delay: 400 //(milliseconds) adjust to the highest acceptable value
};
$(window).scroll(function(){
if( ! loading && scrollHandling.allow ) {
scrollHandling.allow = false;
setTimeout(scrollHandling.reallow, scrollHandling.delay);
var offset = $(button).offset().top - $(window).scrollTop();
if( 2000 > offset ) {
loading = true;
var data = {
action: 'be_ajax_load_more',
page: page,
query: beloadmore.query,
};
$.post(beloadmore.url, data, function(res) {
if( res.success) {
$('.post-listing').append( res.data );
$('.post-listing').append( button );
page = page + 1;
loading = false;
} else {
// console.log(res);
}
}).fail(function(xhr, textStatus, e) {
// console.log(xhr.responseText);
});
}
}
});
});
view raw load-more.js hosted with ❤ by GitHub

3-4 I’m creating an element with a class of .load-more and sticking it at the bottom of my post container (.post-listing). We’ll detect how far away the user is from this element to determine when to load more posts
5-6 Create some variables we’ll need. Setting page = 2 since we’ll be loading the second page. The loading variable will tell us if we’re actively waiting for the next set of posts, so we don’t send multiple requests.
7-13 Instead of running the next block of code every microsecond the user is scrolling, I’m setting up a timer. When scrolling, it is only allowed to check every 400 microseconds (this can be adjusted if needed).
15 If the user is scrolling…
16 And we’re not already loading posts, and the timer will let us check…
17-18 Reset the timer
19 Calculate distance between the window’s current location and the .load-more element
20 If we’re within 2000px of the .load-more element…
21 Set loading to true since we’re about to load new posts
22-27 Setup the data we’re passing to our WP function. We’re including the action, the page we want to load, and the other query variables.
28 Make the AJAX request to the proper AJAX URL and pass along our data
29-33 If the AJAX request is successful, stick the returned data at the bottom of our post container, then move the .load-more element below it (resetting the distance between the window and the button). Increase our page count (so next time we’ll get page 3), and set loading to false.
34-36 If we don’t get a success message back, do nothing (for now). Uncomment the console.log line and whatever information is received will be displayed in your browser’s console.
37-39 If it fails, do nothing (for now). Like above, you can uncomment the console.log for more details about what went wrong.

And that’s it! We now have infinite scroll working on archive pages. For reference, here’s the full code

Infinite Scroll – Click to Load More

On Brody Law Firm’s website (not live, coming soon), at the bottom of an individual post we display more posts from the same category and a button to load more. The code to power this is very similar to the above example, except we’re triggering it on click rather than on scroll.

infinite scroll on Brody Law Firm

<?php
/**
* Javascript for Load More
*
*/
function be_load_more_js() {
if( ! is_singular( 'post' ) )
return;
$query = array(
'post__not_in' => array( get_queried_object_id() ),
'category_name' => ea_first_term( 'category', 'slug' ),
'posts_per_page' => 3
);
$args = array(
'url' => admin_url( 'admin-ajax.php' ),
'query' => $query,
);
wp_enqueue_script( 'be-load-more', get_stylesheet_directory_uri() . '/js/load-more.js', array( 'jquery' ), '1.0', true );
wp_localize_script( 'be-load-more', 'beloadmore', $args );
}
add_action( 'wp_enqueue_scripts', 'be_load_more_js' );
/**
* AJAX Load More
*
*/
function be_ajax_load_more() {
$args = isset( $_POST['query'] ) ? array_map( 'esc_attr', $_POST['query'] ) : array();
$args['post_type'] = isset( $args['post_type'] ) ? esc_attr( $args['post_type'] ) : 'post';
$args['paged'] = esc_attr( $_POST['page'] );
$args['post_status'] = 'publish';
ob_start();
$loop = new WP_Query( $args );
if( $loop->have_posts() ): while( $loop->have_posts() ): $loop->the_post();
be_post_summary();
endwhile; endif; wp_reset_postdata();
$data = ob_get_clean();
wp_send_json_success( $data );
wp_die();
}
add_action( 'wp_ajax_be_ajax_load_more', 'be_ajax_load_more' );
add_action( 'wp_ajax_nopriv_be_ajax_load_more', 'be_ajax_load_more' );
/**
* First Term
* Helper Function
*/
function ea_first_term( $taxonomy, $field ) {
$terms = get_the_terms( get_the_ID(), $taxonomy );
if( empty( $terms ) || is_wp_error( $terms ) )
return false;
// If there's only one term, use that
if( 1 == count( $terms ) ) {
$term = array_shift( $terms );
} else {
$term = array_shift( $list );
}
// Output
if( $field && isset( $term->$field ) )
return $term->$field;
else
return $term;
}
view raw functions.php hosted with ❤ by GitHub
jQuery(function($){
$('.post-listing').append( '<span class="load-more">Click here to load earlier stories</span>' );
var button = $('.post-listing .load-more');
var page = 2;
var loading = false;
$('body').on('click', '.load-more', function(){
if( ! loading ) {
loading = true;
var data = {
action: 'be_ajax_load_more',
page: page,
query: beloadmore.query,
};
$.post(beloadmore.url, data, function(res) {
if( res.success) {
$('.post-listing').append( res.data );
$('.post-listing').append( button );
page = page + 1;
loading = false;
} else {
// console.log(res);
}
}).fail(function(xhr, textStatus, e) {
// console.log(xhr.responseText);
});
}
});
});
view raw load-more.js hosted with ❤ by GitHub

I’m only enqueuing this script on single posts. For the query, we’re requesting posts in the same category, excluding the current post. Also note how the Javascript file triggers the AJAX query when the .load-more element is clicked, rather than on scroll.

Any Questions?

I hope this in-depth tutorial helps you implement infinite scroll in your own projects.

Bill Erickson

Bill Erickson is the co-founder and lead developer at CultivateWP, a WordPress agency focusing on high performance sites for web publishers.

About Me
Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Reader Interactions

Comments are closed. Continue the conversation with me on Twitter: @billerickson

Comments

  1. Nick Wilmot says

    UPDATE:

    If I run ( remove_all_filters(‘posts_orderby’); ) before the new WP_Query(), it’s all fine and dandy!

    Thanks again for your help:-)

  2. Yuri Yeleyko says

    Hey Bill, is there any chance you can help with same for a different infinite scroll scenario:

    – user reads individual post.
    – use reads to the bottom of the post and then new post appears. Not excerpt, but full content.
    – then next post and so on and so for.

    Thank you!