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. Jordan says

    Awesome man! Works like a charm! Thank you!

    I did have one question related to the markup of the additional posts. In the be_ajax_load_more() function, I’m outputting a modified version of the genesis loop -> http://pastie.org/10868812

    On line 17, I have printf( '', genesis_attr( 'entry' ) ); but this does not output the entry class. Is this a scope issue?

    I was able to target a different class to apply the css I needed but curious why it doesn’t output and what the best way to output it would be.

    Thanks man!

    • Bill Erickson says

      Hmm, that’s weird, everything looks right to me. I never use that though. I’d use implode( ' ', get_post_class() )

    • Gabe Lloyd says

      I’m getting the same issue. I couldn’t see the pastie example you posted. Where did you put the `implode(‘ ‘, get_post_class())`?

      • Bill Erickson says

        In the post above, I use be_post_summary(); to represent the actual output of the post. It would be inside that function that I would output implode( ' ', get_post_class() ) on the post entry.

        • Gabe Lloyd says

          Thanks, Bill. I’m just running a regular get_template_part() within the new loop where you are calling the be_post_summary() function. All my content areas within the templates use the post_class.


          if( $loop->have_posts() ): while( $loop->have_posts() ): $loop->the_post();
          get_template_part( 'template-parts/post/content', get_post_format() );
          endwhile; endif; wp_reset_postdata();

  2. Tony says

    Hi Bill,

    Thanks for sharing this. It works great, but I’m having trouble with a modification and I’m hoping you can point me in the right direction.

    I’d like the “Load more posts…” button to load a different amount of posts that page 1. For example, if I land on the blog page and see 10 posts, I’d like the load more button to load an additional 5 posts per click.

    I tried some things with posts_per_page and offset, but it was doing strange things. I’ll keep working on it, but if you have any pointers on how best to approach this I’d really appreciate it.

  3. Gerhard says

    Hi Bill

    Firstly, thanks you for the article, you saved my butt there. 🙂

    I just have one question, how would I check to see if I reached the last page? That way I can add a statement to not show the “load more” button again.

    G

    • Bill Erickson says

      $wp_query->max_num_pages shows you the maximum number of pages for the current query.

      So instead of using Javascript to add the “Load More” button (line 20 of load-more.js), include it in the ajax function be_ajax_load_more(), and before displaying it make sure $wp_query->max_num_pages > $_POST['page']

      • Tony Eppright says

        How would you recommend adding the check so that it doesn’t repeat itself? Since be_ajax_load_more() is being duplicated, adding the button in the function repeats itself also.

        • Nick Davis says

          First of all thanks to Bill for an awesome tutorial.

          Tony – I don’t know if this is the best way (Bill might tell me this is horrible 😉 ) but I was working on something similar just last night (still in local development), at the moment I’m using jQuery to remove the first instance of the Load More button when the script successfully runs.

          (And of course the ‘second’ (new) Load More button only loads if there’s more posts to show anyway, as per Bill’s advice above).

        • Bill Erickson says

          In the javascript file, right after you set loading = true, add a line to remove the button, like $('.post-listing .load-more').remove();

        • Tony Eppright says

          Thank you guys for pointing me in the right direction. That didn’t work for me, but I was able to pull in $wp_query->max_num_pages via wp_localize_script and use that for a conditional statement in the load-more.js script.

          Added a loading spinner and new posts fade in, working great now! Thanks again for your help!!!

  4. Darci says

    I’m getting an error trying to implement this on MAMP: http://localhost/wp-admin/admin-ajax.php 500 (Internal Server Error)

    My debug log says: Error in GetAllFileNamesFromDirectoryWithFileMask.

    I have plugins on my site that use AJAX, and they’re still working properly.

    Any chance you’ll know what to do? I’m new to the dev side of things and I feel this is getting nitty gritty.

    Thanks in advance

  5. Juan says

    Hi Bill, Excellent article. I wanted to ask, how does the be_post_summary() function work? does it return the markup for the loop? Do you create that function elsewhere?

    • Bill Erickson says

      Yes, it handles the markup for the posts. I didn’t include it in the code above because the specific markup I used isn’t relevant to the tutorial and everyone’s implementation will use different markup.

      If the function will only be used for the infinite scroll then I put it in the same file with all the other infinite scroll code. But often times it’s a generalized function used to display content anywhere throughout the site (main loop, related posts after post, infinite scroll…) in which case it’s usually in inc/post-summary.php in my theme.

  6. Ritchie says

    For a not so good in programming. This does not help me. There are no instruction on how to add in the front-end of front-page template. Reading comments doesn’t help either. 🙁

  7. Ritchie says

    I made it work but the loop doesn’t reset or stop. and also, it ruins my parallax background image.

  8. Nick Berry says

    I tried the 1st method and got an error that says “Uncaught TypeError: Cannot read property ‘top’ of undefined”. After spending too much time to resolve it I ended up having to use Jetpack to meet my deadline. I would like to use this method in the future if I can figure it out.

    • simon says

      i think your error is to do with these 2 lines:

      wp_enqueue_script( ‘be-load-more’, plugins_url( ‘/js/scripts.js’, __FILE__ ), array(‘jquery’), ‘1.0’, true );
      wp_localize_script( ‘be-load-more’, ‘beloadmore’, $args );

    • Gabe Lloyd says

      I had a similar issue. You have to create the before setting it to the variable button.

      I did something like $('#main').append( '' );
      var button = $('.load-more');
      so the creation of the class is done before the variable.

      Hope that helps.

  9. simon says

    im trying to implement this for the template archive.php, how would this template file look? do I still use the loop in there?

    • Bill Erickson says

      Yes, the loop produces the first page’s results. The infinite scroll simply loads the additional page’s results as you scroll or click.

      The only thing you need to remove is the pagination links. It might be a good idea to do that using Javascript so that if the JS doesn’t load right, it gracefully degrades to standard paginated posts.

      • Jessica says

        I’m using a Genesis Child theme. I’m also interested in what Simon asked, is there any tutorial online that shows what the php would look like modified to load the archive/categories using infinite scroll for Genesis while hiding pagination?

        Even how to apply it to the footer after comments of a single post? I’m assuming you could somehow use the Genesis hooks but have no idea of how to modify it.

        • Bill Erickson says

          Both of the tutorials above apply to the main Genesis loop. One shows how to load more posts as the user scrolls, and the other shows how to load more after the user clicks a button.

          What specifically are you looking for that’s not covered above?

          • Jessica says

            Thank you for your response Bill.

            I added the first functions script and javascript file to see what happened once I did, but I after checking, it didn’t modify anything in my child themes behavior.

            I managed to identify the css triggers:

            .content
            .pagination
            .archive-pagination div.pagination-next a (so long as I’m using a Prev/Next pagination rather than numbers)
            .post

            But I’m not sure how to use your provided code to infinite scroll the:
            a) Category/tag pages (instead of number pagination)
            b) After single posts (after the comments section)

  10. Terrell says

    Hi, I tried this method and it didn’t do anything. I was looking to add infinite scroll on my single.php so that when the user scrolls to the end of the post it loads the previous post for at least 10 post.

    I tried the second method and nothing happened. I placed the code in my functions.php and I also created the load-more.js

    Can I get some assistance. I have been trying to figure out how to do this for a week and I have tried all the free wordpress plugins for this and none of them work as I like.