Load More Posts with AJAX. Step by Step Tutorial. No plugins.
When my blog subscriber asked me about a such tutorial, I was wondering if there are any other analogue tutorials over the internet. And what I found in Google, surprised me.
Reading all those tutorials one thing becomes clear for me – AJAX is not simple at all. But it is not true!
Step 1. Load more button
Just skip this step if you want to load more posts on scroll.
We 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 ( $wp_query->max_num_pages > 1 )
echo '<div class="misha_loadmore">More posts</div>'; // you can use <a> as well
?>
On the image below you can see that I decided to work with Twenty Seventeen theme because it is well-designed and simple enough.

I inserted the button just under the standart pagination (for TwentySeventeen – 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;
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;
}
Step 2. Enqueue jQuery and myloadmore.js. Pass query parameters to the script.
Well, that’s where the magic happens. This small piece of code allows you to pass the according parameters to the script that’s why the button can work on any page – tags, categories, custom post type archives, search etc. You can also use it for WooCommerce to load more products.
function misha_my_load_more_scripts() {
global $wp_query;
// In most cases it is already included on the page and this line can be removed
wp_enqueue_script('jquery');
// register our main script but do not enqueue it yet
wp_register_script( 'my_loadmore', get_stylesheet_directory_uri() . '/myloadmore.js', array('jquery') );
// now the most interesting part
// we have to pass parameters to myloadmore.js script but we can get the parameters values only in PHP
// you can define variables directly in your HTML but I decided that the most proper way is wp_localize_script()
wp_localize_script( 'my_loadmore', 'misha_loadmore_params', array(
'ajaxurl' => site_url() . '/wp-admin/admin-ajax.php', // WordPress AJAX
'posts' => json_encode( $wp_query->query_vars ), // everything about your loop is here
'current_page' => get_query_var( 'paged' ) ? get_query_var('paged') : 1,
'max_page' => $wp_query->max_num_pages
) );
wp_enqueue_script( 'my_loadmore' );
}
add_action( 'wp_enqueue_scripts', 'misha_my_load_more_scripts' );
If you still do not know, the above code is for your functions.php
. And please note, my load more button is adapted for the main loop only. If you need it for a custom loop, ask me in comments.
You can also pass ajaxurl
like this admin_url( 'admin-ajax.php' )
. Many forget about this parameter and got an error “ajaxurl is not defined”.
Step 3. myloadmore.js – what is inside?
It is a small JS file, actually you can place it anywhere you want, for simpleness I decided to place it just in a theme directory (line 9 of previous code).
I also think that I should give you a choice – a load more button or just load posts by scroll.
Option 1. 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(){
var button = $(this),
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. No button, just load posts on scroll (lazy load)
jQuery(function($){
var canBeLoaded = true, // this param allows to initiate the AJAX call only if necessary
bottomOffset = 2000; // the distance (in px) from the page bottom when you want to load more posts
$(window).scroll(function(){
var 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++;
}
}
});
}
});
});
Step 4. wp_ajax_
This is the AJAX handler function. Insert it to your functions.php
file.
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!
}
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}
If you have any questions, please check comments below.
More WordPress AJAX Tutorials

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
Hey Misha,
great work you are doing here. Thanks for sharing your knowledge!
Are you planning to write a post where you combine the load-more-posts with the ajax-filter?
At the moment I’m playing around combining those to functions.
Hi Stefan,
Thank you!
Here it is! 🙃
Hello Misha,
Brilliant post dear. This helps me alot and saved my day :) Please share the code for the custom query as well.
Thanks
Hello,
ok :) glad to help.
There is no ready for everyone code because it depends on how much loops do you want to use and on many factors actually. The algorithm by steps:
Step 1. Variables
For each loop you have to duplicate variables. If you use
$q
custom query, then, after the query add something like this:Step 2. JavaScript
I think it is better just to duplicate the “Step3–Option1” part of the post with the changed variables for each loop individually. It will be the most simple solution.
That’s all. PHP code can be the same, unless you would like to use different post entry HTML templates.
Thank you very much !! Misha. Solved it :)
That’s great! You’re welcome :)
I am getting a problem. By default i am showing 6 posts. But when i am clicking on ‘Load More’ the 6th data is going to last and from 5th data 6 more posts are loading. After that all data are loading in order and just fine.
Can you please give me a solution?
Hi,
there could be different reasons, check if you configure the correct amount of posts to show in Settings > Reading.
You can also try to display the first page in your AJAX results (just set the
paged
parameter to 1) – so, you will see, if there is a post that shouldn’t be there etc.I have solved it already.
The issue was on js code.
i have updated this line
with
and it’s working just fine now.
Thank you for your help :)
You’re welcome :)
Thankyou so much for this great post!
If I could be as cheeky as to ask how one would fade the posts in? Any suggestions :)?
I tried
however this just fades in one post not the group that gets ajax loaded.
Thanks for looking!
Hi James,
thank you for a good question! When I return the comments in ajax, I add one more class to new comments – so, when AJAX receives the data, comments already has this class.
After that in jQuery:
Thanks so much for your reply Misha! Getting closer too!
My only issue is trying to append the class “.newcomment” to these newly loaded articles.
I tried;
This seems to fade in all posts as the article element is being targeted. So I need to target only the newly loaded ajax posts! Sorry my javascript is very bad as I am pretty much a beginner.
Thanks for looking!
No, no, set this class in PHP :)
does this work with isotope?
Hi Maria,
isotope.js? I think yes, if you will use
iso.arrange();
after loading new posts.Thanks Misha, I will try!
Hi, Misha,
Can you explain why it’s better to use query_posts instead WP_Query in this case?
Thanks so much for your tutorial!
Hi Bruno,
In two words –
query_posts()
better emulates the main loop, so it has the better chance that all the code included withget_template_part()
will work great.And we use it inside the AJAX processing function, it means that
query_posts()
won’t affect anything on the page.Hello Misha…
Superb article, to the point explanation … You are a magician … Thanks a lot for sharing this
Hello Ravi,
I’m happy to help :)
Thank you so much :)
Hi!, one question, how do i put my html code insted calling the function
get_template_part( 'template-parts/post/content', get_post_format() );
?Hi,
You can put it directly instead of
get_template_part()
.Okey, thanks! I will try it.
Thanks Misha for the awesome tut!
I also had issues with its implementation for the custom post types.
So these tips given by a non-pro might be helpful.
1. You dont need function misha_my_load_more_scripts.
Just connect your .js file manually in the footer or header. (<script src="…
2. Add
to your template file. Not to misha_loadmore_ajax_handler function, not needed there!
3. In the .js file, replace variable names
with your own variables:
That should make it.
Forgot about
to
Thanks for the response, i have finally figured this out. Here is my code, working on both a custom query on homepage and defaults posts static page.
In functions.php I changed this function to wrap in is_home() conditional since that was working great:
For my custom query in front-page.php:
and for my loadmore.js:
I’ve been working on this for two days so I’m feeling really good haha, I was ready to bang my head against the wall!
Thanks for sharing this Brooke, helped me out no end with my custom template pages :)
..also thanks Misha for suppling the script in the first place :)
Always welcome! 🙃
Thanks for sharing your code and experience! :)
I’m glad you guys have figured it out!
Hi guys,
I did used your code for a custom loop with a custom post type on a custom page. I get the good query_vars of my loop. But when I load more posts, I don’t get the wanted custom post type (of my initial loop) but any of my custom post types.
That is really strange because every other information of my initial loop (retrived in my query_vars) are well interpreted (number of pages, orderby …).
Any idea that can cause that ? Otherwise, my button succeed in loading more posts in ajax (It is just not the good ones :D )
Thank you in advance !
Hi Arthur,
Fast solution – try to add
post_type
parameter in PHP :)Hi Misha, thank you for your quick answer. I don’t know why my query_var is alterated (looks correct in my html page. My first try was to override the parameter :
in my function loadmore_ajax_handler(), but without success.
So the only solution I found was to stop retrieving the query_var of my page and write $args in my function.
Thanks for this tut ! :)
Hi Arthur,
You’re welcome! I’m glad you’ve found the solution :)
Hi is it possible to combine 2 articles from the ajax call? Now the output will be:
article
article
I would like to do the following:
article
article
Hi Ronald,
Sorry but I’m not sure that I understand you. But I think the answer is yes, just set the posts_per_page parameter to 2.
Hi I see, that my div wrappers were missing:
Yes, sure, just create an incremented variable before the loop, and in each loop iteration check – if the variable is odd or even and add
</div><div>
.Hello,
How can I use it to my blog page?
Hello Berat,
I think you have to begin with reading this post, I know, it is not that simple.
Hello Misha,
It works on archives and category pages and search to but I have a custom page blog that I call posts from custom category and how can I put the perimeter current_uri just like you did ?
Thank you.
All the parameters are passed with
wp_localize_script()
function and it should work on all pages, no matter if it is a category archive page or all posts page.I suppose that you use custom loop, if so, try to pass parameters to the script directly, without
wp_localize_script()
function. This was already discussed in comments, please look.I understanding that,
I have just one variable: cat => $x
How can I pass it directly without wp_localize_script() ?
#comment-1055
Thanks Misha,
I checked out and I used that code but when I click load more button it keeps calling the same posts because on the second click of load more button the query data is coming with blank variables,
I used print_r() to see the incoming result
I figured it out Misha,
Thank you so much for this post and for not hesitating to help me.
Ok, I’m glad you’ve figured it out :)
Hello, I would like to add a my theme (not TwentySeventeen).
That I have to do?
Hello,
The steps are the same.
Hi Misha,
thanks for the tut! I have implemented your script, but when i click the Button, i get following error:
Uncaught ReferenceError: misha_loadmore_params is not defined
at HTMLDivElement. (neo-loadmore.js:7)
at HTMLDivElement.dispatch (jquery-1.10.1.min.js:5)
at HTMLDivElement.v.handle (jquery-1.10.1.min.js:5)
Any ideas?
Many thanks,
André
Hi André,
It looks like you skipped Step 2 or made a mistake there.
misha_loadmore_params
object is generated withwp_localize_script()
function.Hi Misha,
thanks! I had forget to wp_register_script. Now it works :)
Hi, this is a great tutorial, first one that’s actually worked! Everything is working fine on Archive pages, any ideas how to implement on a Custom Query Loop that’s been already setup?
Hi Paul,
the answer is in this comment.
Thank you for the tutorial! Have you got a link to myloadmore.js as I can’t find it?
Thanks
Hi Mat,
This file should be in your current theme directory. You have to create this file yourself and add the code from Step 3 in it.
Hello.
Thank you,
This is not working for me on a custom post type archive. It loads more posts, but it loads the standard blog posts instead of the testimonials.
Forgot to add
global $wp_query;
Works fine now! Thanks.
I haven’t manage to get it fully working… Maybe is because I’m using wp_query? I have to use the later could you add the code required for wp queries to work?
Thanks and I’m so glad I found your article!!
Hi,
If you use WP_Query, you have to include the parameters directly to the
<script>
tag, do not usewp_localize_script()
function in this case.hi,
First of all, thank you so much for this wonderful article. I am pretty new with these staff.
So instead of
what should we write within tag for example when I fetched an array of posts ($posts) using WP_Query. I really appreciate your time :)
Hi,
I think this comment should help.
Thank you so much :) . I’ll try that. Is this snippet work with network query as well I mean for multi-site posts?
Yes, sure 🙃 But a little custom coding required.
Hi Misha,
it is not working for me , i am using it in custom post type , please help.
Hi Rahul,
I hope this comment thread will help you
Hello! thank you for this article, it was very useful for me. But I have a small problem – one of the posts loads two times, you can see that here on the website – http://khaleejesque02.wpengine.com/people/. By the way, I use such array – $cat_query = new WP_Query($cat_args);
It would be great, if you can help me. Thanks!
Hi Anastasia,
What is your posts_per_page parameter in Reading settings?
4 posts selected.
But, when I choose 5 in the Reading settings, everything is ok. Why does it so?
Please check if there is
posts_per_page
parameter in your code and change it to4
or toget_option('posts_per_page')
.Thanks.
First thanks for the code but I am having one problem. When I clicked the more button, it loads the remaining posts but just above the last one not after the last post.
Can you help me?
Hi,
You have to customize this line of code:
Basic jQuery knowledge is required but try to replace
before
withafter
.hey can you help me the code only work on index.php(home)
how to make it work on other page ?
Hey,
did the load more button appear on other pages?
ah its fixed sorry for the trouble :C
Great tutorial thanks! Is there a way I can add a fade content when it loads?
Thanks you. Yes, sure, make it hidden by default and then use jQuery
fadeIn()
method.Great tutorial! I could not figure out how to remove the current content and replace with the new (loaded content) instead of just adding to the top of page. Any insights will be greatly appreciated.
Hi Marcos,
Just use jQuery
.html()
method.Hey! Thanks for the article.
Is
wp_localize_script()
varcurrent_page
set one time? If I have multiple pages of results it’s seeming to just continue loading the second, because this variable isn’t updating, which means that thepage
in my ajax is going to always be the same?What am I missing?
I see! The variable is manually incremented in JavaScript as well.
Hi Misha,
Brilliant tutorial! Is there any way to adapt it to WordPress comments? I have some posts with over 200 comments and I would like to have such a “Load more” button: I would display the first 20 comments for example then display the rest of them 20 by 20 (or more) when someone clicks the “Load more”.
If you have time at some point to write a tutorial about it… well :) Thank you!
Hi Lou 🙃
Ok, I will add it to my tutorial plans. It would be great if people who are interested in this tutorial will leave a reply here as well.
+1 support for this request, its good to have . thanks in advance Misha.
Finally, here it is!
Hey mate,
this is great! I got it working great and then went back to it and it doesn’t work on my custom query but works on my standard archive pages. Any ideas why that might be? (I can’t remember how I fixed it the first time and have no idea what I did to mess it up haha) Happy to send through code if you need it :)
Hey,
Thank you 🙃 The code from this post is only for the main loop, but you can find advices on how to use it for custom queries in comments, for example in #comment-1055 and in the other ones.
Hi mate,
Thanks for getting back to me. Sorry to be a pain.. and you must be so over it, but I applied all the stuff in all the comments and managed to get it so it loads posts for all my custom queries. However, the posts that display are just the next posts in order of date after the last post in the initial page load. So essentially, it looks like the custom query args aren’t getting passed through when the new posts are loaded. Any ideas why that might be? In particular I’m trying to load posts in order of most comments within the last six months. Is there any way to debug to see what args are being loaded when the button is clicked so I can start to figure out where I’m going wrong?
Thanks a tonne!!
Jarrad
Hey,
Usually I remove any unformatted code, sorry … 🙃
Look, that’s what you have to do by steps:
wp_reset_postdata()
function print the JS variables, it may look something like this:console.log()
any variable by button click.You can use Step 4 from the tutorial without changes I think.
Great article, thank you.
I’ve noticed that once more posts are loaded, the next batch to display are displayed before the last previous post.
Ie. I display 6 posts – 1,2,3,4,5,6
When more are loaded, they are appearing as – 1,2,3,4,5,7,8,9,10,11,12,6 rather than 1,2,3,4,5,6,7,8,9,10,11,12
Any ideas?
I have same problem. But if you don’t remove
the_posts_pagination();
then its working.It looks just like HTML markup issue,
Find this line in your code (Step 3, Option 1):
And replace with:
But of course it is better to learn jQuery and DOM traversal a little.
Perfect, that was it. Thank you!
Solved. Great!
Hey,
Thanks for the tutorial and your support. Unfortunately I can’t get it to work with the (vanilla) theme grid magazine. Do you have any idea, why it won’t work?
I tried both ways but nothing happens.
Thanks!
Hi Rupert,
If nothing happens, something in your jQuery script. Check your browser console and read comments.
Thanks for pushing me in the right direction ;)
Just for others:
I had to modify the line
to
Thanks again for the great tutorial!
Always welcome! 🙃
Hi, thanks for the excellent code and tutorial. I got this working for the main loops (blog posts) and also on the archive for a custom post type. The only issue I am having is on the CPT archive, whenever the posts load a ‘0’ is added to the screen. I created a GIF showing what is happening. https://imgur.com/a/Yzc3Z
Hi,
Did you add
die()
function at the end ofwp_ajax_
function? (Step 4).Hi,
I did yes. Just found the error though, it was my own bonehead move. When I was testing I left a duplicate of the click function, with an error in it. Thanks for the reply.
Very nice tutorial. Did the trick for me. With a bit of extra effort I managed to get it all working with Masonry.
Hi! How did you get it working with Masonry?
Hi Adrian,
It was discussed here #comment-1350.
Do you have any video for this tutorial ?? I want to add it into divi theme blog module .. Basically i had first implement it on index page. When I click on view more button , nothing happned . Then I check the console .. I got following error ..
ReferenceError: misha_loadmore_params is not defined
It means that you failed at Step 2.
Hi, im also looking to implement this in my divi theme, but for a page – no posts just made my own “posts” snippets on a page i set as my blog but for every 5 “manually created post excerpts” I would like to have a load more button or next page. How can I do this?
Thanks!
the previous issue is solved but i need multiple load post in a / different page how to configure that ..
OK i have no idea 😊 can you tell me the steps you did to make it work in divi? Thanks!
actually i had create custom blog module then implement it on module
Did you figure it out? Im also trying to do something like this for Divi, would love to know if it works?
i was remove it and use divi built in ajax pagination then remove newer entry link and replaced old entry link text with load more button …
Can’t thank you enough for this post!
This does not seem to keep track of pagination in the url if I’m not mistaken? I mean if you go to let’s say /blog/page/2 will it load the second page of post? And when you click the ‘load more’ button the url/history should be updated. Important for SEO.
If you would like to make the load more button SEO-friendly, it is better to use it with pagination. More info in this tutorial.
Hi Misha, I got a problem with order and orderby, I am trying to change the order, in args of php and js, but it keeps same, do you know how to change de order?
An update:
I found that my request is always
"ORDER BY wp_posts.menu_order, wp_posts.post_date DESC"
, and it only happens using ajax. I tried to put orderby in all argurments and reseting wp_query but I can`t change the order.For who having same problem, that the posts inside ajax is not ordering, you have to suppress the filters, because it goes through ajax-admin and add some weird order filter. Just add in args:
'suppress_filters' => true;
Hey Misha! Thank you for the amazing tutorial! It helped me a lot!
Hey Misha, this is so good. Thank you so much for putting this online.
I got it working in custom theme, but now I want to use it on different pages where the pages only show posts from a certain category (only ever 1 cat per page) but I am really struggling as to where I pass the category from the page to the scripts so that when I click the button to show more it only shows more of the same category.
I have tried playing with the code and got no-where, I am not great at coding so I am hoping it is a simple thing to do but I am just not getting it.
Hey Dave,
The code from this tutorial should be OK for any archive page… So, I do not know how to help you in a comment. You can contact me and my team will help you as an option.
Hi,
What do you expect in
$_POST['query']
. It is returning an array which gives error in stripslashes function.Thanks
Hi,
Loop arguments are there.
Hi, Thanks for this, it works fine for me on my “page_for_posts” but I get a server error when I use the same code on a category archive page?
Hey Jonny,
This could happen when you run a WordPress action/filter hook and using a function in it which is also contains the same hook.
Hi thanks for this tutorial. I followed all the steps I do not get any errors on the console but when I click the button it just changes the text to loading and removes the button but does not load the posts. I think the if statement in myloadmore.js validates to false and it executes the else —button.remove();
Please help
Sorry never mind looks like I forgot to replace the get_template_part() function with my custom HTML. Thank you a lot