Load More Posts with AJAX. Step by Step Tutorial. Without Plugins

In this tutorial, I am going to show you how you can implement asynchronous post loading in WordPress or even an infinite scroll.

Please consider that there could be two different implementations for the AJAX load more button and it depends on the theme you’re using today. For example:

  • If you’re using old-fashioned themes, then you can create a load more button with a little bit of PHP and JavaScript code (and that’s exactly what we’re going to do in this tutorial).
  • If you’re using block themes (FSE themes), then the whole implementation comes down to a custom block which will represent our load more button. I am going to talk about it as well.

No matter which way you choose, upgrading your AJAX load more button into an infinite scroll is not a big deal, by the way.

1. AJAX Load More Button (HTML and CSS)

To be more specific, it is not even an “AJAX load more button” yet, but just a static “load more button”.

This step is optional if you prefer to use an infinite scroll instead.

So, let’s begin with the button HTML. Here is just one main rule – do not show the button if there are not enough posts. We will check it with $wp_query->max_num_pages.

<?php
global $wp_query; // you can remove this line if everything works for you
 
// don't display the button if there are not enough posts
if( 1 < $wp_query->max_num_pages ) {
	echo '<div class="misha_loadmore">More posts</div>'; // you can use <a> as well
}
?>

In the image below you can see that I decided to work with the Twenty Seventeen theme because it is well-designed and simple enough.

WordPress load more posts with AJAX
The load more button which is styled for the Twenty Seventeen WordPress theme.

I inserted the button just under the standard pagination (for Twenty Seventeen – index.php line 55), but you can remove it as well. To style the button the according way use CSS below.

.misha_loadmore{
	background-color: #ddd;
	border-radius: 2px;
	display: block;
	clear: both;
	text-align: center;
	font-size: 14px;
	font-size: 0.875rem;
	font-weight: 800;
	letter-spacing:1px;
	cursor:pointer;
	text-transform: uppercase;
	padding: 10px 0;
	transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.3s ease-in-out;  
}
.misha_loadmore:hover{
	background-color: #767676;
	color: #fff;
}

2. Enqueueing JavaScript (And Passing Query Parameters to the Script)

First of all, later in this tutorial, we are going to write plenty of JavaScript code that is going to actually auto-load posts on scroll or on button click and it is clear enough that we need to do two things right now:

  1. Include our JavaScript file.
  2. Pass query parameters into the script.

Not sure that I should remind you how important it is to pass the current query parameters to the script, I mean, we are going to use our AJAX load more button on different WordPress pages – homepage, category and tag pages, search results page, etc.

Include JavaScript

Here we’re just enqueueing myloadmore.js which you should create manually in your theme. We’re using the wp_enqueue_scripts filter hook for that. And if you don’t know where to use this code, please read this article.

add_action( 'wp_enqueue_scripts', 'misha_my_load_more_scripts' );

function misha_my_load_more_scripts() {
 
	// register our main script but do not enqueue it yet
	wp_register_script( 
		'my_loadmore', 
		get_stylesheet_directory_uri() . '/myloadmore.js' 
	);
 
 	wp_enqueue_script( 'my_loadmore' );

}

The question that might come to your mind here is why we are using both wp_register_script() and wp_enqueue_script() functions here when we can use just one of them? Soon you will find out.

Pass query parameters into the JavaScript

At this moment you have to decide – are you going to use AJAX load more for main queries only or for some custom WP_Query as well?

In order to use it for main queries it is enough to pass the query arguments via wp_localize_script() function.

Just like that:

global $wp_query;
wp_localize_script( 
	'my_loadmore', 
	'misha_loadmore_params', 
	array(
		'ajaxurl' => admin_url( 'admin-ajax.php' ),
		'posts' => json_encode( $wp_query->query_vars ),
		'cur_page' => get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1,
		'max_page' => $wp_query->max_num_pages,
	)
);

On the other hand, if you’re going to use it for custom queries built with WP_Query, you will either need to add an additional <script> tag with every custom query parameter or to include the parameters directly into the button using data- attributes.

Some of these approaches have already been discussed in the comments section.

3. JavaScript Code for AJAX Load More and Infinite Scroll

I know, guys, that some of you don’t like jQuery very much, but this is more like a tutorial for beginners, so I am using jQuery to show how it works. If you already know JavaScript and would like to use the no-jQuery approach, you can easily modify the code below for your needs.

Option 1. AJAX load more button

If you choose the option 1, skip the option 2 and vice versa.

jQuery(function($){ // use jQuery code inside this to avoid "$ is not defined" error
	$( '.misha_loadmore' ).click(function(){
 
		const button = $(this)
		const data = {
			'action' : 'loadmore',
			'query': misha_loadmore_params.posts, // that's how we get params from wp_localize_script() function
			'page' : misha_loadmore_params.current_page
		}
 
		$.ajax({ // you can also use $.post here
			url : misha_loadmore_params.ajaxurl, // AJAX handler
			data : data,
			type : 'POST',
			beforeSend : function ( xhr ) {
				button.text( 'Loading...' ) // change the button text, you can also add a preloader image
			},
			success : function( data ){
				if( data ) { 
					button.text( 'More posts' ).prev().before(data) // insert new posts
					misha_loadmore_params.current_page++
 
					if ( misha_loadmore_params.current_page == misha_loadmore_params.max_page ) { 
						button.remove() // if last page, remove the button
					}
					// you can also fire the "post-load" event here if you use a plugin that requires it
					// $( document.body ).trigger( 'post-load' );
				} else {
					button.remove() // if no data, remove the button as well
				}
			}
		})
	})
})

Please note that line 23 can be different for your theme, it depends on your HTML document structure. I think you should know some basic jQuery DOM traversal methods – prev()next()parent() etc.

Option 2. AJAX infinite scroll

jQuery(function($){
	let canBeLoaded = true // this param allows to initiate the AJAX call only if necessary
	const bottomOffset = 2000 // the distance (in px) from the page bottom when you want to load more posts
 
	$(window).scroll(function(){
		const data = {
			'action' : 'loadmore',
			'query': misha_loadmore_params.posts,
			'page' : misha_loadmore_params.current_page
		}
		if( $(document).scrollTop() > ( $(document).height() - bottomOffset ) && canBeLoaded == true ){
			$.ajax({
				url : misha_loadmore_params.ajaxurl,
				data:data,
				type:'POST',
				beforeSend: function( xhr ){
					// you can also add your own preloader here
					// you see, the AJAX call is in process, we shouldn't run it again until complete
					canBeLoaded = false
				},
				success:function(data){
					if( data ) {
						$( '#main' ).find('article:last-of-type').after( data ) // where to insert posts
						canBeLoaded = true // the ajax is completed, now we can run it again
						misha_loadmore_params.current_page++
					}
				}
			})
		}
	})
})

4. wp_ajax_

This is the AJAX handler function.

add_action( 'wp_ajax_loadmore', 'misha_loadmore_ajax_handler' ); // wp_ajax_{action}
add_action( 'wp_ajax_nopriv_loadmore', 'misha_loadmore_ajax_handler' ); // wp_ajax_nopriv_{action}

function misha_loadmore_ajax_handler(){
 
	// prepare our arguments for the query
	$args = json_decode( stripslashes( $_POST[ 'query' ] ), true );
	$args[ 'paged' ] = $_POST[ 'page' ] + 1; // we need next page to be loaded
	$args[ 'post_status' ] = 'publish';
 
	// it is always better to use WP_Query but not here
	query_posts( $args );
 
	if( have_posts() ) :
 
		// run the loop
		while( have_posts() ): the_post();
 
			// look into your theme code how the posts are inserted, but you can use your own HTML of course
			// do you remember? - my example is adapted for Twenty Seventeen theme
			get_template_part( 'template-parts/post/content', get_post_format() );
			// for the test purposes comment the line above and uncomment the below one
			// the_title();
 
 
		endwhile;
 
	endif;
	die; // here we exit the script and even no wp_reset_query() required!
}

Load More In Block Themes

So, all the code above in this tutorial was related to “classic themes”, but is it possible to create an AJAX load more button for block themes as well?

The answer is definitely yes, but I am afraid I can only provide you with a plugin solution here, which is my Simple Load More Block plugin which I developed. And let me explain why.

When working with block themes, the only way to add a load more button without plugins is to develop a custom Gutenberg block which will be the child block for the standard WordPress “Query Loop” block, preferably – with the Interactivity API support. Probably, it is not even a part of an extra tutorial but a topic for a whole video course 🙃

Here is how you can add a load more button block with my plugin:

Add a load more button in WordPress block themes
“Fullwidth posts with uppercase titles” is just a renamed Query Loop block. So basically we just added an editable “Load More” block in it, and now can load posts with AJAX on site pages.
Misha Rudrastyh

Misha Rudrastyh

Hey guys and welcome to my website. For more than 10 years I've been doing my best to share with you some superb WordPress guides and tips for free.

Need some developer help? Contact me

Follow me on X