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.
Carson says
Hello Bill,
This seems like a very useful plugin, but what I’m looking for in particular is one that keeps loading posts much like your “BE Full Post Scroll” plugin. Problem is, the GitHub repository no longer exists.
Initially, I tried implementing the first option of this infinite scroll plugin, and I was returned with:
“Uncaught TypeError: Cannot read property ‘top’ of undefined
at load-more.js?ver=1.0:19”
I decided to implement the “functions.php” code into the actual functions.php theme file, and the error pops up whenever I reach the bottom of the post, likely indicating its use. Regardless, is there any chance you still have the necessary functions available for the scroll used on the Denton Forum posts? I feel that option would be a better fit for my website after the redesign.
Bill Erickson says
Not sure why the repo was set to private, but I just made it public: https://github.com/billerickson/be-full-post-scroll
Carson says
Thanks for the quick reply, Bill.
I tried saving the plugin as a ZIP and installing it conventionally (upload), but nothing appears to work in terms of navigation. According to the source code, it DOES call the JS functions from ScrollSpy, leaving behind this CDATA element.
/* */
Said URL:
https://atomicbranding.co/uncategorized/test-2/
Carson says
EDIT: Sorry, should have seen that coming when I left the syntax.
var args = {“container”:”.content”,”post”:”.entry”,”next”:”.post-navigation a[rel=\”prev\”]”,”offset”:”2000″,”delay”:”400″,”debug”:””};
/* ]]>
Bill Erickson says
You’ll need to customize the arguments to match the class names used in your theme. Use the
be_full_post_scroll_args
filter, see here.Carson says
Okay, so far I have been able to load more pages, but the result seems far from your example, the Denison Forums. For instance, only one additional page will load, the URL stays the same, and the header and footer are duplicated. Any particular reason why? Wrong class attributes?
Bill Erickson says
Yes, it sounds like you aren’t using the correct classes.
– Container is the element to which the newly retrieved post is appended. The default is .content
– Post is the element that is the post; this is appended to the container
– Next is the link that contains the URL for the next post you want loaded. You should use get_previous_post_link() somewhere on the page, and use this parameter to target that link
If you need additional help implementing this, I recommend you hire someone on Codeable.
Roselle says
Hi Carson,
Did you ever get this to work? I’ve been going over the classes again and again and again –I’m about to break my keyboard in half! Maybe I should sleep, it is 6am after all.
I’d greatly appreciate your help!
Thanks,
Roselle
Carson says
Thanks for the tip. In regard to the link I provided above, is there any chance you could point out which classes should be assigned to the $args array based on the source code? I believe that might be the part where I’ve had trouble in the past.
sunshine says
Dang. I can read php and javascript and understand what I am reading. I am comfortable tweaking, deleting, adding php code and am building a theme from scratch right now, slowly. Began as a vanilla html markup coder many, many years ago. But, while I want to do this all myself and get why this is better than plugins, which I tend to hate after many happy years with Drupal, I guess it’s best for me to go to Codeable also. Can you tell me, though, what skills should I look for in a codeable developer to effect this infinity scroll for a home page that will be displaying a 3-col grid of different custom content types. That will help me narrow my search. Thank you in advance.
Bill Erickson says
A familiarity with WordPress and Javascript.
But really, there are plugins that do a good job of infinite scroll. I believe Jetpack has a module for it. This tutorial is really for developers who want to build it themselves. I recommend trying existing plugins before hiring someone to build it custom.
Melissa says
I agree Bill, there are some good free plugins for this. I actually really like Ajax Load More ( https://wordpress.org/plugins/ajax-load-more/)
This plugin uses the same method for loading posts as you outline in your tutorial and you can use the same functions to display posts which makes it easy to match the styling.
Thanks for the great tutorial BTW.
Diah says
Thanks for the tutorial, Bill.
I’m trying to implement it my archive page but I’m not quite sure what I’m doing wrong.
Does this feature override the “Blog pages show at most” setting in “Reading Settings” on the WP Dashboard?
Do I have to do anything to my index.php page?
Right now it looks like this:
get_header();
if (is_single()) {
get_template_part(‘partials/loop’, ‘single’);
} else {
get_template_part(‘partials/archive-loop’, ‘index’);
}
get_footer();
and my archive-loop-index.php page has a 3 column layout.
Thanks again!
Megan says
Hi Bill,
Thanks so much for this post, and for taking the time to answer comments !
I have read through all of the comments but am still struggling to fully implement this solution for a custom query on a page. It works beautifully if I hard code the args in be_ajax_load_more. Any direction on how to pass the appropriate query arguments would be much appreciated!
Bill Erickson says
Pass the query arguments using
wp_localize_script()
, like this.Roselle says
Bill, O Mighty WordPress/Genesis Guru…
Thank you so much for your time and effort posting this, and helping us as much as you have already. I know you don’t have to debug any of our errors (for free, I might add), but thanks for responding to our comments and helping as much as you can.
With that being said, I wont take up too much of your time, just hoping you could point me in the right direction…
Okay, so I am using your ‘be-full-post-scroll’ plugin, filtered it with my own arguments, and created the ‘content-partial.php’ template.
I can get the next (actually previous) post to load, BUT it loaded the site header and site footer too. So I removed them in the template. After that, it seemed like everything was fine –except that when I looked at the page source, it was still outputting the html meta and scripts of the next post. Also, the URL in the address bar doesn’t change at all. 🙈
Looking over the previous comments, I could it be that the classes in my $args are wrong…? But when I look at the code, the classes .content and .entry should work. I just don’t even know. What can I do?
Thanks in advance for your help!
Bill Erickson says
It sounds like your partial is loading an entire page rather than actually a “partial” page. Inside that file you should only output the markup you need. If you’re using genesis, don’t call the
genesis();
function as that will load EVERYTHING.Roselle says
Bill,
Thanks for pointing me in the right direction re: the template. I’m no longer calling genesis(); so I got that part working fine now.
I still have the issue of the URL not changing on scroll down or up (and back/forward button functionality as an extension of the URL change).
If you/anyone here knows how to remedy that, I’d greatly appreciate it 😇
Thanks,
Roselle
Albert says
Thanks for this tutorial, but as I am fairly new to ajax, javascript and php, it would appreciate if you can give me a simple example of be_post_summary(), just to give me some idea where to begin
Thank you
Bill Erickson says
be_post_summary()
contains whatever you want displayed for each post. For instance, if you were just listing the post title it would be: https://gist.github.com/billerickson/40d5a8fbf27c45e5795a3448fe960849Albert says
Thank you for the answer, just i cannot implement it in the index.php part of the template
Any help on how to do it?
Albert says
Sorry just an error form my part, I have commented out the be_post_summary().
But it does not work, because if i delete $args[‘paged’] = esc_attr( $_POST[‘page’] );, it loads the same 3 posts over and over each time i click the button, and I re insert $args[‘paged’] = esc_attr( $_POST[‘page’] ); it just loads only one post and thats it
Any help?
Bill Erickson says
I recommend looking at what arguments you’re passing to the AJAX function, specifically the ‘page’ parameter.
Jordan says
I did get this working great for search, but I can not seem to get this working for custom taxonomy searches – I am not getting back anything in my array. I console logged out res, and the JS is successfully loading but my data is empty.
https://gist.github.com/bigredboots/279e27eeffc120534affd77b3730175c
Ankish Kumar says
Everything gets working for an infinite scroll, but I also want a GIF image before additional sets of posts are loaded. Is this possible?