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.
- 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)
- The AJAX query. This is the Javascript actually asking for the new content, and then inserting it into the page.
- 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.
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' ); |
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' ); |
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' ); |
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); | |
}); | |
} | |
} | |
}); | |
}); |
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.
<?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; | |
} |
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); | |
}); | |
} | |
}); | |
}); |
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 says
When you get to
loading = true;
(here), you could add$(button).addClass('active');
, and then remove the class when you get toloading = false;
here.Then in your CSS, do something like
.loading.active { background: url(loading.gif) no-repeat center center;}
.Ankish Kumar says
Thank you so much for taking the time to reply. As you have instructed I have implemented it like this with the CSS suggestions →
https://s3.amazonaws.com/stackoverflowfiles/_done_it_like_this.png
.loading.infinite_active { background: url(./img/loading.gif) no-repeat center center;}
But eventually it didn’t worked.
Sridhar Katakam says
Implementation of this tutorial (user scroll as the trigger) in Genesis:
https://sridharkatakam.com/infinite-scroll-in-genesis/
yliac says
Hi Bill,
Thanks for the informative post.
I have ajax infinite scroll built into my theme on a viral video website https://viralchop.com/ and im trying to figure out how to customize the trigger to load more posts around halfway down the page instead of once the user reaches the bottom of the page?
Any insights are much appreciated
Cheers
Bill Erickson says
On this line of the Javascript file, the “2000” refers to how many pixels from the bottom you should trigger the AJAX query. Increasing this number will make the query trigger sooner.
You could also get creative with it. Instead of hardcoding a number, you could do half the height of the screen, or half the height of the article’s content, or anything else you want.
Mike says
I’ve gone through this tutorial + the comments numerous times and I must be missing some steps that are assumed. I get the first two steps (update functions.php with the two functions and create the load-more.js file), but then what? How do you then modify the theme files to actually implement it? Assume a completely vanilla self-hosted WordPress install with infinite scroll going on a typical blog page.
Bill Erickson says
After you have modified the theme to load the JavaScript files and define the AJAX response in functions.php (which it sounds like you have done), you simply need to update
.post-listing
in the JavaScript file to use the correct class names for whatever HTML element contains all your posts.Mike Dunn says
That pointed me in the right direction. Working great. Thanks.
Berk says
Hello Bill, first of all thank you for your writing and your help. I use the “load more” script but how do I remove the load more button after the posts are exhausted ?
Bill Erickson says
In your load-more.js file, remove the line where the button gets added back (here).
In functions.php, find your function that runs during the AJAX query and add the “load more” button only if there are more pages in the query (see here).
Berk says
Thank you
grant tailor says
Getting POST 500 error on admin-ajax.php
can you explain more how to get the class .post-listing and also .load-more?
that part if very important in this working but you left that part out
Bill Erickson says
Those classes have been added to my template files. You’ll either need to add them to your template files in the appropriate place, or review your theme’s existing classes and use a class that’s already there (ex: .site-main might be used instead of .post-listing).
Imran Chaudhary says
Thanks for this tutorial but I’m getting the same as a few other people:
POST /admin-ajax.php 500 (Internal Server Error).
Any suggestions?
Bill Erickson says
You’ll have to look in your PHP error log, or use
console.log
throughout your JavaScript to help debug the issue.Yesh says
Hi Bill, Thanks so much for the very insightful tutorial. I am trying to develop an infinite scroll and am having trouble, kind of similar to https://www.billerickson.net/infinite-scroll-in-wordpress/#comment-674359 and my exact problem is this https://stackoverflow.com/questions/54282847/passing-custom-variables-in-woocommerce-storefront-theme-based-infinity-scroll-f. I see that the query parameter that needs to be passed is already set, not empty and being passed but it doesn’t work as expected. Can you please let me know, what to do
Tim says
The 500 error appears because bp_post_summary(); doesn’t exist, which gets called in be_ajax_load_more();
You’ll need to replace it with your own template data or template PHP file. For me, it was loop-template/content.php:
get_template_part( ‘loop-templates/content’, get_post_format() );
Nick Wilmot says
Hello Bill,
As always, thanks for this post!
What’s the best way of changing the ‘orderby’ value?
Using the ‘pre_get_posts’ hook only works for the initial set of posts, and not the posts loaded via the AJAX load more.
Bill Erickson says
In the be_load_more_js() function we’re including all the relevant query parameters, so you could include your orderby there as well.
Nick Wilmot says
Thanks Bill, hmm I still have something going askew somewhere. I’m on a big learning curve with JS at the moment.
I’ve added:
‘orderby’ => ‘title’
//to the “$args” in the be_load_more_js() function, which in this case is placed on a Genesis CPT archive template.
orderby: beloadmore.orderby,
//to the JS
$args[‘orderby’] = esc_attr( $_POST[‘orderby’] );
//to the AJAX function in functions.php
Bill Erickson says
That all looks right to me. You’ll need to do some debugging to see where it’s going wrong.
(Do these separately)
At the end of be_load_more_js(), add
print_r( $args ); exit;
to see what’s actually in the args.In the JS file, add
console.log( beloadmore );
to see what’s being received by the JS file (look in the browser’s console – open your browser’s inspector and press ~)In the AJAX function, either add the received data to the error log (
error_log( print_r( $_POST ) );
) or temporarily return the $_POST data (wp_send_json_success( $_POST );
) and in the JS file, log the response (if( res.success ) { console.log( res.data ); }
)That should help you track down where its being lost.
Nick Wilmot says
Thank you for your insight Bill, I just learnt a few new debug tips:-)
The ‘orderby’ $arg value of ‘title’ is definitely passing all the way through to the AJAX function, and I can echo it out from within the AJAX loop. Maybe syntax isn’t accepted by WP_Query for the orderby value, as even if I override it to ($args[‘orderby’] = ‘title’;), it’s ignored and default ordering is applied?
The posts in the AJAX loop are correct, it’s subtracting the first page fine, and not duplicating anything. They’re just not running in the correct ‘orderby’ sequence
Bill Erickson says
‘title’ is a legitimate orderby option (see here).
The only thing I can think of is something is hooked to pre_get_posts and overriding the orderby parameter. Hook your own function into pre_get_posts with a priority of 999 and
print_r( $query ); exit;
to see what’s happening.Nick Wilmot says
By syntax I meant the way by which the $args are presented is not as per the codex for a WP_Query. However, when I strip that out and replace it with an array as per codex, with hard values set, it still won’t behave and respect the ordering! And nothing abnormal reporting in pre_get_posts either.
Something quirky going on somewhere! I’m sure I’ll resolve it eventually.
Thanks so much for your advice, Bill… I learnt a few things and really appricate your input.