AJAX Filter Posts by Category with Pagination
In this tutorial we’re going to learn how to create an AJAX pagination (without page refresh) on your WordPress website and also how to make it work together with AJAX filters (if you have one of course).
As a result of this tutorial, we’re going to have something like this:

On the screenshot, you can see that we’re going to use page numbers links as a pagination, but using the same approach, you can easily make it with a load more button.
Creating a WordPress AJAX Pagination without a Plugin
First things first, we will begin with creating an AJAX pagination without any plugins. Depending on whether you’re using a block theme or a classic theme, the whole process can be absolutely different.

Using the Query Loop block in Block Themes
In order to create an AJAX pagination without plugins for a block theme all you need to do is to open the Query Loop block settings and make sure that the “Force page reload” toggle is unchecked.

That’s basically all you need to do 😁 How cool is that? The only thing is that if your Query Loop block contains any blocks that don’t support Interactivity API, then probably it won’t be possible until you remove those blocks.
Creating an AJAX Pagination for Classic Themes
Since we decided that we’re going to create a WordPress AJAX pagination without any plugins, this part can be a little bit complicated, especially compared to the previous part, at least because here we need to manually send AJAX requests.
First thing first, let’s assume that we have the following HTML elements in our classic theme:
nav.pagination– it is a<div>element with page numbers navigation, each link has a.page-numbersCSS class and a current one has.currentclass.#site-content– it is where the posts are displayed.
So, every time we send an AJAX request when using a pagination we need to populate both HTML elements with the updated content.
Ok, let’s try to send an AJAX request right now. I’m going to use jQuery for this purpose here in order to simplify the whole thing.
jQuery( function( $ ) {
$( 'body' ).on( 'click', '.page-numbers', function( event ) {
// prevent links from being clicked otherwise a page will be reloaded
event.preventDefault()
// also do nothing if we are already on this page
if( $(this).hasClass( 'current' ) ) {
return
}
// in most cases we can get a page number we want to navigate to from a link text
const pageNumber = $(this).text()
$.ajax( {
url : pagination_and_filter_args.ajaxurl,
data : {
'action' : 'misha_paginate',
'query': pagination_and_filter_args.query,
'page' : pageNumber,
},
// this data type allows us to receive objects from the server
dataType : 'json',
type : 'POST',
beforeSend : function( xhr ){
// we can add some preloading animation here
},
success : function( data ){
// display posts
$( '#site-content' ).html( data.posts )
// display an updated new pagination
$( 'nav.pagination' ).html( data.pagination )
}
} )
} )
} )Probably at this point, you may have the following questions:
- Where to use this code?
- What is
pagination_and_filter_args? - How to process this AJAX request with PHP?
So let’s start step by step, and let me answer the first two questions first.
I recommend you create a separate JavaScript file for this code and enqueue it the following way in the functions.php of your theme:
add_action( 'wp_enqueue_scripts', function() {
global $wp_query;
wp_register_script(
'my_pagination_and_filter',
get_stylesheet_directory_uri() . '/my_pagination_and_filter.js',
array( 'jquery' ),
time(),
true
);
wp_localize_script(
'my_pagination_and_filter',
'pagination_and_filter_args',
array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'query' => json_encode( $wp_query->query_vars ),
)
);
wp_enqueue_script( 'my_pagination_and_filter' );
} );As you can see, with the help of the wp_localize_script() function I also included the pagination_and_filter_args parameter, so it will be available in our JavaScript file.
The only pitfall here is that providing the value of a query variable like this will only allow you to use this AJAX pagination for the main loop. So, if you’re going to create any additional loops on your page with WP_Query, then you need to look for some alternative ways to provide the query variables into the AJAX request. You could probably try to use a paginate_links_output filter for that purpose. Or just use a block theme 😁
Last but not least let’s process this AJAX request in PHP:
// add_action( 'wp_ajax_{ACTION VALUE}', ...
add_action( 'wp_ajax_misha_paginate', 'rudr_ajax_pagination' );
add_action( 'wp_ajax_nopriv_misha_paginate', 'rudr_ajax_pagination' );
function rudr_ajax_pagination(){
// prepare our arguments for the query
// query_posts() will take care of the necessary sanitization
$args = json_decode( stripslashes( $_POST[ 'query' ] ), true );
// update a page number
$args[ 'paged' ] = $_POST[ 'page' ];
$args[ 'post_status' ] = 'publish';
$posts_html = '';
query_posts( $args );
if( have_posts() ) :
ob_start(); // start buffering because we do not need to print the posts now
while( have_posts() ): the_post();
// adapted for Twenty Twenty theme
get_template_part( 'template-parts/content', get_post_type() );
endwhile;
$posts_html = ob_get_contents(); // we pass the posts into a variable
ob_end_clean(); // clear the buffer
endif;
$pagination_html = get_the_posts_pagination(
array(
'mid_size' => 1,
'prev_next' => false,
)
);
// no wp_reset_query() required
echo json_encode( array(
'posts' => $posts_html,
'pagination' => $pagination_html,
) );
die();
}In this code snippet, I would like you to pay attention to the following moments:
- If you don’t provide a post status
$args[ 'post_status' ] = 'publish', then drafts are also going to be included, which will definitely break our pagination. - The best way to get a post template in a classic WordPress theme is to use the
get_template_part()function, but this function can not return its value, it always prints it, that’s why we have to use buffer functionsob_start(),ob_get_contents()andob_end_clean()so we can runget_template_part()multiple times and then add everything that was printed to a variable. - Instead of
json_encode()PHP function you can feel free to usewp_send_json_success()if it is more convenient for you. get_the_posts_pagination()is also a very convenient function when we need to print a pagination within a loop, usingpaginate_links()doesn’t make sense here.
Combining AJAX Category Filter with Pagination
Ok but if you want to filter posts by category with pagination, all in AJAX? Is there a simple way to combine an AJAX filter with an AJAX pagination?

Once again, there are two completely different approaches depending on whether you’re using a classic or a block theme.
AJAX filter posts by category with pagination in Block Themes
If you’re going to code everything from scratch here, then I have bad news – you need to learn Gutenberg block development deeply together with Interactivity API.
So here I would recommend you to just use my Taxonomy Filter Block plugin instead which works great with the default AJAX pagination in WordPress block themes.
All you need to do is to install and activate the plugin and add the taxonomy filter block into a desirable query loop block. What is really great here is that it works for additional loops on the page not only for the main loop.
AJAX filter with pagination in Classic Themes
Probably here it would be better to read a tutorial about creating an AJAX category filter first. I think it is not necessary to copy all the code I had already described there, so I am going to skip the HTML part. Everything else we’re going to uncover below.
Ok, first of all, let’s create a JavaScript event with an AJAX request for a category filter:
jQuery( function( $ ) {
$( '#ajax-filter select' ).change( function() {
const categoryId = $(this).val()
$.ajax( {
url : pagination_and_filter_args.ajaxurl,
data : {
'action' : 'misha_filter',
'query': pagination_and_filter_args.query,
'category_id' : categoryId
},
// this data type allows us to receive objects from the server
dataType : 'json',
type : 'POST',
beforeSend : function( xhr ){
// we can do some preloading animation here
},
success : function( data ){
// display posts
$( '#site-content' ).html( data.posts )
// display an updated new pagination
$( 'nav.pagination' ).html( data.pagination )
// change the query vars
pagination_and_filter_args.query = data.query
}
} )
} )
} );And now, let’s process this AJAX filtering request in PHP:
// add_action( 'wp_ajax_{ACTION VALUE}', ...
add_action( 'wp_ajax_misha_filter', 'rudr_ajax_filter' );
add_action( 'wp_ajax_nopriv_misha_filter', 'rudr_ajax_filter' );
function rudr_ajax_filter(){
// prepare our arguments for the query
// query_posts() takes care of the necessary sanitization
$args = json_decode( stripslashes( $_POST[ 'query' ] ), true );
$args[ 'cat' ] = $_POST[ 'category_id' ];
$args[ 'post_status' ] = 'publish';
$args[ 'paged' ] = 1;
$posts_html = '';
query_posts( $args );
global $wp_query;
if( have_posts() ) :
ob_start(); // start buffering because we do not need to print the posts now
while( have_posts() ): the_post();
// adapted for Twenty Twenty theme
get_template_part( 'template-parts/content', get_post_type() );
endwhile;
$posts_html = ob_get_contents(); // we pass the posts to variable
ob_end_clean(); // clear the buffer
endif;
$pagination_html = get_the_posts_pagination(
array(
'mid_size' => 1,
'prev_next' => false,
)
);
// no wp_reset_query() required
echo json_encode( array(
'posts' => $posts_html,
'pagination' => $pagination_html,
'query' => json_encode( $wp_query->query_vars ),
) );
die();
}The code is very similar to the one we used before when we were creating an AJAX pagination without a filter. The only thing to keep in mind is that we have to update the query parameters pagination_and_filter_args.query after filtering posts otherwise, the pagination won’t work.
Misha Rudrastyh
Hey guys and welcome to my website. For more than 15 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
Instead of having a link in the pagination like this one: /resources/page/2 when using your example, the links are being generated like this: /wp-admin/admin-ajax.php/page/2/
Any thoughts on why this is happening?
Thanks misha.
Here’s my vanilla JS version of the jQuery code: