How to Filter Posts by Category in WordPress
In this guide, I will dive deep into the process of creating an AJAX category filter for a WordPress website. We will do it both with the help of plugins and without plugins at all.
Method 1. AJAX Category Filter – WordPress Plugin (Recommended)
Let’s start with the plugin approach. I would say that it is a recommended method, because nowadays the block themes in WordPress prevail, and if you need to filter posts by category the proper way you can not just edit your theme template files like index.php or something; instead, you need to create an interactive Gutenberg block which is a child block for the “Query Loop” block and it should interact with the query loop similar to how the “Pagination” block does. Under an “interactive block,” I mean a block that uses the Interactivity API.
However, developing an interactive Gutenberg block from scratch is not an easy task; you need to learn at least the basics of React and something about creating Gutenberg blocks.
So, here are the two plugins you can use for that goal:
- Category Filter Block (Free) – allows you to create AJAX category filters with some basic features.
- Simple Taxonomy Filter Block (Premium) – a more advanced plugin that allows you not only to work with custom taxonomies but also comes with a whole bunch of customization options.
Below is an example of an AJAX post filter by category created with my plugin for a website with the default WordPress block theme installed – Twenty Twenty-Four:

If you decide to use the premium version of the plugin, then you can do much more; for example, you can use a multiselect with checkboxes in your filter the following way:

All you need to do to allow this multi-selection with checkboxes is use a single block setting:

This AJAX category filter plugin for WordPress also comes with a bunch of other useful features, for example:
- You can not only filter posts by category but also filter these posts in the same query loop by tags or any other custom taxonomy at the same time (in other words, you can add multiple filters into the same loop).
- Filters can also be displayed as a list of links or as buttons with fully customizable styles.
- You can hide or show post counts.
- You can sort the categories (or terms) in your filter in the desired order.
And so on…
If I got your attention, you can check the Simple Taxonomy Filter Block plugin yourself; however, if you’re undecided, here is a demo page with more examples.
Method 2. Filter Posts by Category in WordPress without Plugins
In the example below, we’re going to use the following:
- Pure JavaScript on the front end. I know, many of you guys don’t like jQuery, and since it is not a filter for a WooCommerce store and the library isn’t included in the front-end anyway, there is no real reason to use it.
- A default “Twenty Twenty” WordPress theme. We can not use the latest themes like Twenty Twenty-Four or Twenty Twenty-Five, because the latest themes are block themes, and in that case, we will need to create a Gutenberg block for our AJAX category filter, which is a completely different story. Currently, I don’t have a tutorial for that, but if you need a category filter for a WordPress block theme, please check the first part of the tutorial.
Below is the preview of how our AJAX category filter is going to look:

Now, let’s break down the process of creating this AJAX category filter step by step.
Display a Category Filter as a Select Dropdown in HTML
Our first goal when we need to filter WordPress posts by category without a plugin is to create some HTML for the filter and then to decide where to insert it into the WordPress theme that we’re about to use (in our case – “Twenty Twenty”).
<div class="ajax-filters">
<form id="ajax-filter">
<?php
$categories = get_terms( // you can use get_categories() function as well
array(
// you can replace the taxonomy parameter value with any custom taxonomy name or 'post_tag'
'taxonomy' => 'category',
'orderby' => 'name',
)
);
if( $categories ) :
?>
<select>
<option value="">Select category...</option>
<?php
foreach ( $categories as $category ) :
?><option value="<?php echo $category->term_id ?>"><?php echo $category->name ?></option><?php
endforeach;
?>
</select>
<?php
endif;
?>
</form>
</div>The question is – where to use this code? Well, it depends on your theme, of course. In “Twenty Twenty” it is better to create a child theme and use it at the very beginning of index.php file.
By the way, it is also possible to combine this code with my multisite queries plugin and its network_get_terms() function. So, all the network categories will be in the select dropdown, and all the network posts will be displayed as the filtered search results.
Send Asynchronous Requests with JavaScript
Now it is time for what? To do some JavaScript stuff!
We actually need to create a form submit event for our filter, which is going to send asynchronous requests to the server, to WordPress’s admin-ajax.php file, and it is actually where we’re going to return filtered posts. But since a select dropdown is the only field in our filter for now, we just need an event when it is changed.
const ajaxFilter = document.getElementById( 'ajax-filter' )
const siteContent = document.getElementById( 'site-content' )
ajaxFilter.querySelector( 'select' ).addEventListener( 'change', event => {
// .is-loading{ opacity: 0.5 } creates that opacity-like effect
siteContent.classList.add( 'is-loading' )
fetch( ajaxurl + '?action=ajaxfilter', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify( {
// filter WordPress posts by category
'cat' : event.target.value
} ),
}).then( response => {
return response.text()
}).then( response => {
if( response ) {
siteContent.innerHTML = response;
}
siteContent.classList.remove( 'is-loading' )
// console.log( response );
}).catch( error => {
console.log( error )
})
} )If you need to filter posts by category (without a plugin – just a reminder) using the jQuery library, you can check my other tutorial where I am doing it for WooCommerce.
In the code above, we also have an undefined variable, which is ajaxurl. Here I assume that its value is the default one in WordPress – /wp-admin/admin-ajax.php, but you can pass the whole value as well, like add_query_arg( 'action', .... Well, I think a little bit of an example won’t hurt.
add_action( 'wp_enqueue_scripts', function() {
wp_register_script( 'mishafilter', 'URL HERE', array(), time(), true );
wp_localize_script(
'mishafilter',
'misha_args',
array(
'ajaxurl' => add_query_arg(
array(
'action' => 'ajaxfilter'
),
admin_url( 'admin-ajax.php' )
)
)
)
wp_enqueue_script( 'mishafilter' );
} );In that case, we will need to change one line in the JavaScript code above:
fetch( misha_args.ajaxurl, {Get Posts Filtered by Category inside wp_ajax_ Hook
Last but not least – the part which goes into the functions.php file of your child theme. Here we’re going to get the filtered posts HTML and return it as a result of our AJAX request.
add_action( 'wp_ajax_ajaxfilter', 'rudr_ajax_filter_by_category' );
add_action( 'wp_ajax_nopriv_ajaxfilter', 'rudr_ajax_filter_by_category' );
function rudr_ajax_filter_by_category() {
$args = json_decode( file_get_contents( "php://input" ), true );
query_posts( $args );
// below is almost unchanged part from Twenty Twenty theme index.php file
$i = 0;
while( have_posts() ) {
++$i;
if ( $i > 1 ) {
echo '<hr class="post-separator styled-separator is-style-wide" />';
}
the_post();
get_template_part( 'template-parts/content', get_post_type() );
}
die;
}This is not a very complicated code, but just in case, I would like to remind you about:
- When we send AJAX requests with
jQuery.ajax, we can access the data from$_POSTglobal variable, but not when we send requests withfetch(). That’s why using the line withfile_get_contents( "php://input" )is essential here. - Also, in this case, I prefer to use
query_posts()instead ofWP_Queryhere, because, anyway, we exit the code withdieat the end, so there is no need to worry about global variables, but it may save the day when loading templates withget_template_part()in some cases. - And yes, I hope you know how
wp_ajax_andwp_ajax_nopriv_actions work – it is just a standard way of handling AJAX requests by WordPress.
If you have any questions about the code or about the plugin, please ask in the comments.
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
Whether this method is effective in a WordPress multisite network?
Hello,
yes, you can use this method with my multisite plugin.
good work
thx
Use the wp-navi, I want a page feed.
Could you?
Hi,
yes, you can use wp-navi or the custom function from this tutorial about multisite pagination.
Is it possible to display all posts at first and then apply a filter to those posts? I’m able to manipulate the WP_Query to display what I need, but only after I hit the “Apply Filter” button.
Thanks in advance.
Great write-up, works like a charm.
How would add a second select for a second Taxonomy?
Just the same as the first, duplicate the necessary lines from each part of the code but do not forget to change taxonomy name.
Hello! Is it possible to increase the amount of posts that return after we apply the category filter? At the moment it seems to default to 10. Maybe an ajax “load more” button would work?
Hi John!
The first option is creating a load more button — I can not describe in two words how to do it here — maybe I will publish a post later about that.
The second option is to change the default WordPress parameter in “Settings > Reading” from 10 to another value.
And the third option is to force
posts_per_pageparameter in the PHP code ($argsarray).Could you please update this to have pagination and also “infinite loading”? I’ll be very thankful!
Hi,
of course, here you go.
Great tutorial mate, I’ve got only one problem:
– the dropdown list is showing categories, but the ajax response returns only data from post_type =>post,
I’ve set the post type to my custom post type – portfolio :
But it’s still not working. Can you please help me?
Hi Maciek,
thanks,
the
post_typeparameter should never be insidetax_query. Add it to the$argsarray the following way:Awesome stuff :) Cheers! :)
One more question – how to submit the form on clicking a radio button?
Try to replace this part of the jQuery script:
$('#filter').submit(function(){to this:
$('radio.your_radio_class').change(function(){Ok thanks :) Last one thing:) Is there a way of showing in list of categories something like ‘All’? So when I click on it it’s gonna show all categories?
Hi Misha,
I use this, for my home page, and Anytime the result is “0”…
Can you help me ?
Thank you so much :3
Hi Beifong,
it means that ajax is ok, but it is not connected to the hook/function. Please read the post one more time and pay attention to the
actionhidden field and hook (add_action()arguments ) names.Works perfect. I have been using this for many times
How can I trigger some jQuery after ajax is done loading?
Thanks!
You can add into the
success : function( data ){ }.This works for me. Thank you!
Hello.
I had the same problem with my custom post type category. Probably, you have already found the answer, but maybe someone else will need it
Thank you Tanya for your useful comment :)
this saved my day !!!! thx you very much ;)
Thanks Misha! I was able to do something very similar and it worked perfectly following your tutorial.
If you have an archive of Movies and the user selects 3 filter options (Horror, Comedy, Documentary), how would they be able to send that page with those filters selected? How would you generate a shareable link?
Looking forward to your response!
Thanks,
Jose
Hi Jose,
actually there are two ways – you can use $_GET parameters in a URL or a hash.
So,
?post_tp=Horror,Comedy,Documentaryor#post_type=Horror,Comedy,Documentary.When the filter page is loading, you’re checking the
$_GET['post_tp'], select the appropriate values in the form and run your custom WP_Query for those parameters.Second way is when you check the parameters in JavaScript
window.location.hash, after that you select the appropriate values in the form and script send the AJAX request.P.S. I do not recommend to use
post_typeas a $_GET parameter name, because there are could be conflicts.Hi Misha,
Awesome filter plug-in. It is doing exactly as I need.
Is there a way to reset the filter, without reloading the page?
Regards,
Rafael
Hi Rafael,
Well, ok, the first step is creating a button or link element with the ID #reset for example.
The second step is jQuery code, something like this:
Hello Misha,
Thank you, but sorry I wasn’t fully clear on my question earlier.
I have used this tutorial now to filter posts based on categories in WordPress.
At first, it displays all the posts from all categories and the filter then displays posts only from a certain category.
Is it also possible to refresh / reload the filter so all posts from all categories are displayed again, and the filter is set back to the default selection?
Thank you
By the way,
I have found a solution to “reset” the dropdown button. If I could also reset to show all the posts, as before selecting a category, that would be perfect
document.getElementById('filter').reset();.I have just re-checked the code and noticed I was missing a
return false.It works now!
After resetting the filter if you want to display posts from all categories, you can just resubmit the form
$( '#filter' ).submit();.I think I figured it out. I was using the Sage 9 theme, which has namespacing. So I had to add a \ to make the function call look like:
$query = new \WP_Query($args);It works perfectly. Thank you so much for sharing this functionality!
Dude, you saved my life! I’m new to Sage 9 and overlooked this namespace business… I owe you a beer!
Misha thanks for sharing this! your posts are so amazing! Keep it up! <3
Always welcome,
Thank you for this inspiring comment!
Hey,
Very good job !
But if I want to use a custom type and not a default post in WordPress?
Where do I add the name of my custom post, please?
Thank you !
Hi,
Thank you!
In Step 3 to the
$argsarray, withpost_typeparameter.Hi Misha,
Love your work – it’s been really useful. One issue I’m having is keeping the standard WooCommerce pagination after filtering my results – any ideas? I’ve merged your loop in your example with the one i’m using on archive-products.php but its still not working
Hi Gemma,
Definitely, it is not simple. I implemented the pagination code in Step 3 for my website but it was painful. And maybe you will need a custom pagination function.
Hi Misha,
Your right it certainly is painful lol! I ended up realising that the standard WooCommerce pagination wasn’t going to be any good anyway as it reloads the page. I found your post about merging filters & load more posts – life saver! keep up the amazing work :-)
Great! 🙃
Thanks!
Thank you for this tutorial! Saved my bacon.
Hello Misha, Thanks for the great tutorial.
I sent you an email but made some progress, and maybe my issue can help others ?
I modified the code to submit the form on input change.
But the problem is that instead of adding the results to the #response div, the page goes to the URL “https://mysite.com/wp-admin/admin-ajax.php”. I assume that it comes from the AJAX request which must have a problem but I can’t find where as the hidden input is there and I tripled checked its value.
Could you take a look ?
Hi Marine,
Sorry, I had to remove your code – it doesn’t contain any useful information for me 🙃 People copy-paste their own code sometimes even without wrapping it with editor buttons hoping that I will write the code for them.
So, I understand your problem and let me point you to the correct way. If the page goes to
admin-ajax.php, you have to check your browser console, in Chrome – right click on any element and then Inspect, choose the Console tab. It shows you all your JavaScript errors in the code.P.S. I don’t remember your emails.
Hello :) Thanks for your response ! (It’s weird, I did wrap the code with the buttons!)
I already checked the console, and what’s weird is that I don’t have any errors !
I figured that it might come from the wp_ajax_ filter, which is why I copied my code thinking it might be relevant !
In the end, I managed to fix my code. I was submitting the form on input change :
<input type="checkbox" onchange="this.form.submit()">and doing the ajax request also on input change,
jQuery('input[type=checkbox]').change(function(){ }so on submit the page followed the action attribute, as the default behavior.
I remove the onchange attribute from the input and it works now.
Thank you again for this great and useful tutorial :)
That’s great, I’m glad you’ve figured it out 🙃 Always welcome!
Do you know how I can get each filter to match the search query? I’m trying to include
's' => $search_queryto each array however I can’t seem to retrieve the search input in the function in functions.php. However I have no problem getting the search input in any other php file. Is this because it hasn’t been loaded in ajax? Any help will be appreciated.Hi!
Superb tutorial, thank you very much! Was exactly what i needed.
Is it possible to dynamically change the button text to “show 123 posts” where 123 is the precalculated number of posts that will be shown?
Hi,
Thank you! :)
I think you have to send one more request, the simplified one. And use this
echo $query->found_posts;to print the number of results.But the best practice is to combine these two requests using JSON response – in this case when you click the button, the posts won’t be loaded because they have been already loaded and will be displayed immediately.
Hi!
Thanks for your reply!
Can you give me a hint of how to combine the two requests?
I already managed to call 2 separate functions – one for the result count and one for the results themselves.
In my understanding there must be a way to just write the HTML results in one variable/array(??) and the result count in a different variable/array(??). Then the ajax function that is called on filter changes returns the result count, and the ajax function that is called by submitting the form returns the HTML results.
Ok, I will try to describe you it in comments, but I think it is better to publish a tutorial about that.
First of all you have to set
dataType: 'json',parameter in you AJAX call.Second – in your PHP
wp_ajax_function create an array of two elements,json_encode()it and print.Third, in your ajax call
success()function you can usedata.htmlanddata.countproperties.Something like that 🙃
Hej Misha,
thanks for that awesome post. I am actually searching for that kind of solution for filtering team members by position and location.
If i read right, in your solution you use a “Apply filter” Button. Is it possible to do the query automatically whenever a checkbox is changed?
Cheers Mike
@Mike Cosgrove
i’ve just applied this tweak on Misha’s code. change:
$('#filter').submit(function(){...into:
$('#filter').change(function(){and it should work.
Excellent code! How to deal with pagination?
Thank you.
The only way to deal with pagination is to create your own custom function for it.
Hello Misha, this was an excellent post, very useful and informative!
I would like to know about dealing with pagination as well. It would seem a logical addition to this otherwise great post, if I might add.
In my case, I have altered your code to suit my needs a little bit, and have tried to deal with the pagination but it gives an error when trying to click the links to the next page, after it successfully loads and shows the posts, AND updates the correct pagination. Instead of taking me to the correct page (i.e. blog/page/2), it takes me to another page (wp-admin/admin-ajax.php?paged=2).
I could add some of the code that I have used if that would help others.
In any case, thank you for your guidance!
Hello,
I receive many requests about pagination, so it seems like the post about it will be published soon.
Ah, excellent news. Thank you!
That post was also clutch, thanks for that.
Hi Misha, I’m getting only a number (0) as the response from the ajax call, not sure why that is, any ideas?
Never mind, I solved it, thanks.
How did you solve it?
In many cases it means that the action parameter from your hidden field:
<input type="hidden" name="action" value="myfilter" />and from action hooks from
functions.phpdoesn’t match:In this example the parameter is
myfilter.Howdy Misha,
Awesome tutorial! Well written and extremely helpful! 🙏
Hello.
First, what a beautiful tutorial, easier than a lot of other solutions I crossed on the web.
Secondly, I’m sorry but I’m not English, sorry in advance for my poor language ;)
Thirdly, I’m not a Dev, or something like that, but I love putting my hands into the code and often it works :)
I spent 10 years on Magento and only discovered WordPress now…. Woua, easier.
I have a question which is simple for you I think, and it’s more about PHP, or how to get data without a “loop”.
In the function file, in your example, you suggest to call the title, and it works fine.
But I want to call a template part with more data and PHP.
I succeeded but on the frontend it calls the template into the template, as many times as there’s a post.
So it breaks all the HTML and I can’t manage the results.
MANY MANY THANKS :)
Hi,
Everything depends on the template part you include 🙃
Usually, while working with
get_template_part()I recommend you usequery_posts()function insteadWP_Query().If the HTML structure is the only issue you faced, you can add the closing/opening div element manually like this:
Thanks a lot for your fast answer, works very fine ;-)
This is a great tutorial. Thank you so much. It works great. Wondering if you have any ideas on how to make a “clear filters” button, so the filters are reset to null and all the posts show.
Thank you! 🙃
Here is a step by step tip on how to implement a “clear filters” button.
Let’s suppose that you would like to add it inside the
<form>element. In this case, you have to use<a>HTML tag as a button.<a href="" id="clear">Clear</a>Then you need to trigger the event. It is about jQuery code. I used selectors and element IDs like in my tutorial. Add it inside
jQuery(function($){ ... });Perfect!
Hey Misha thank you for sharing with us this amazing example.
I have a question, is there a way that i can check if select dropdown is selected with a specific value?
Hey,
I removed your code because you inserted it unformatted.
Do you mean this?
if( isset( $_POST['nameofselect'] ) && $_POST['nameofselect'] == 'UK' )Hmm something like this, but all-cities is an option value not the name of the select, should it be like this?
Updated
Thank you very much, that really worked for me :)
Hi Misha,
Thanks for this!
Is there any way that I can simply use a list of category links to switch out posts?
i.e. clicking on a category will filter the posts
Hey Pete,
The answer to your question is in the comments.
Thanks Misha,
I just want a list of links though, not a dropdown. Would it work the same way?
Yes, just use the
click()event instead ofchange().Thank you so much!
Hi Misha,
Are you planning for any tutorial on how to create WooCommerce product filters based on different attributes?
Thanks
Ganesh
Hi Ganesh,
But WooCommerce allows to create this type of filter with widgets.
Hello Misha, thank you for your excellent blog posts! They are very helpful, and I am so happy to be able to filter posts by category in WordPress without a plugin.
My question is if you have any idea or if it is even possible to capture the filter selections so that you could pass them between pages.
I have a separate query showing all posts before anything is filtered, and I have a search field and select boxes to filter taxonomies. So say I choose term1 and term2, then click a blog post from the results. Then I click a back button to return to all the posts. I want the posts to be filtered with the same selections.
I noticed that if you hit the back button, the select boxes retain their values, but the query still shows all the results.
Any knowledge you may have would be appreciated.
Thank you so much!!
Hey Beth,
If you would like the filter to save the value when you click back button, you must change the filter page URL when you filter posts. You can do it without AJAX
?param1=value1or with changing a hash#param1=value1Hi,
How I can use multiple radio buttons with the same name and different values?
Regards,
Alex
Hi Alex,
The radio buttons you described can be used just like a regular
<select>element in your PHP code.Hi Misha,
I’ve rarely come across a tutorial or article where the author/dev gives so much personal time and code advice to commenters. It’s really impressive and thanks so much, this tutorial is going to be a great resource for me.
I have a few questions:
1. Instead of putting the misha_filter_function in the functions.php file, wouldn’t it make more sense to put that into a plugin? I’ve never seen a loop query done from within the functions.php file, and I’m wondering if that’s the most performant way to do it.
2. I have a custom post type archive page where I want to do the filtering, and instinctively I want to put the loop query code directly in the archive template. If I wanted to do that, how would I go about keeping the misha_filter_function in the functions.php file, while having the loop inside the archive template?
Thanks!
Hey Zach,
Thank you 🙃
1. Yes, definitely it could be done in a plugin or in a child theme. But in fact, there is no difference unless your theme receives updates.
2. Nope,
misha_filter_function()should be always in thefunctions.php/ plugin files.Awesome. Thanks for your response. :)
I was hoping you could help me with some advice/code snippet:
I have a custom post type, accommodation, and I have categories and tags as taxonomies for this CPT. I can get the category dropdown select to work or several tags as radio buttons. But how do I cross-filter by both categories and tags? When I create my tax_query it overrides the other tax_query, how do I merge them into a single cross-filtering tax_query so that I can filter by tags and categories both? Hope I’m explaining this correctly.
Easy-breezy :)
Hi Misha,
How could we adapt the Ajax handler and Javascript code if the way to select the categories would
<input>checkboxes and not with<select>tag?Thank you!
Hi Mat,
When using multiple checkboxes, it will not be enough just to sent a selected category ID in the AJAX request with the
catparameter. It would probably be better to send the entire form there. Also, may need to create an$argsarray manually (with thetax_queryparameter) before sending it to thequery_posts()function.However, this feature is already implemented in the plugin approach.