Skip to main content

Filter WordPress posts by custom taxonomy term with AJAX and pagination

This is a repost of original post AJAX Filter Posts By Tag.
There was a lot of interest about that post so I decided to further improve code example and make it more simple.

In this example I will list all posts by taxonomy post_tag using a shortcode, example also includes pagination.
This example will filter all posts by a single tag only, meaning you are not able to filter posts by multiple tags at a same time, I will cover that case in the next tutorial to keep things simpler to understand.
Here you can check working example of Filter WordPress posts by custom taxonomy term with AJAX.

1. Shortcode

Shortcode is used to display list of available tags and container where posts will be inserted after we get them with AJAX.
Shortcode accepts 4 parameters:

taxonomy we want to get terms from
list of comma separated term_id’s, in case you would like to enable filtering by only certain terms
term_id you would like to be initially selected when page loads
how many posts will be displayed per page

Usage in WordPress page: [ajax_filter_posts per_page="1"]

Or if you would like to use it in your template file: <?php echo do_shortcode('[ajax_filter_posts per_page="1"]'); ?>

Basically this is just a get_terms function from WordPress Codex and some additional markup that is needed to make this work properly.
Each anchor tag contains taxonomy and term slug in data attribute, this is required so we can get these data with jQuery and get results for clicked tag.

If you look at the source you will notice that tag looks for example like:
<a href="?path-to-tag" data-tax="post_tag" data-term="term-slug">

2. Javascript

Javascript file consists of 2 parts.

  1. get_posts($params) which makes AJAX request and returns results
  2. Binding get_posts function to tag list and pagination

On click…

On click we need to get taxonomy, slug and page number. We need these so we can construct WP_Query.
These parameters are then passed to our custom javascript get_posts($params) function.

get_posts – make a call and display results

I wanted to keep function very basic, just to get and display data, you can add the fancy stuff yourself.
Firstly we define selectors, then pass the params, do ajax and display result if any.
If there will be error, it will be shown in status div.

3. WP_Query and pagination

First of all, if we want to make pagination work we need to slightly modify WordPress function.
The idea is that when someone clicks on pagination link, from that link we need to extract only page number we want to retrieve.
To make this happen we use WordPress function paginate_links to generate exact markup we need.

Custom pagination markup

The following function will generate page links with following markup:
<a href="?paged=1">

And it’s very easy to get page number from this link with jQuery (we do that in step 2 – On click…).
We get ‘href’ attribute from clicked link and strip all except numbers to get our page number.


This is the function we call with javascript which creates query and returns post in JSON format.
I will not explain to much, you should be able to see from the source what’s going on there.

  1. Verify nonce
  2. Sanitize inputs
  3. Construct WP_Query and do the standard WP loop
  4. Return result

And you are done

Here you can check working example of Filter WordPress posts by custom taxonomy term with AJAX


Full source code in single file can be found on github:

  1. PHP file gist
  2. JS file gist

Just keep in mind that you need to enqueue and localize your javascript file.


  1. Hi, thank you for tutorial, in the previous tutorial about ajax , without pagination,
    i’m getting only one post displayed, after clicking in tag, even if i have 10 posts in tag.
    How to get this filtering working without using shortcode?
    I’ve read that using shortcodes are bad for performance, beacase wp will look in all shortcodes,
    and this may slow down page, if you using them often
    If i change adding class active into toggleClass , then can i have filtering by many tags at once working?

    1. Hi Gabrielle,

      1. If you don’t want to use shortcode just call the function vb_filter_posts_sc() and pass params as an array.
      2. changing ‘toggleClass’ will not enable filtering of multiple tags, it picks up value only of clicked tag, but instead you would need to pick up all tags with class ‘active’

    1. Hi Gabrielle,

      After you get all :checked elements with jQuery you can simply save them to local storage with javascript:

      // Put the object into storage
      localStorage.setItem('myTerms', JSON.stringify(myTermsObj));

      // Retrieve the object from storage
      var retrievedObject = localStorage.getItem('myTerms');

      // View terms in console
      console.log('retrievedObject: ', JSON.parse(retrievedObject));

    1. It would be possible but you need to update html/js code slightly:
      1. in HTML id of containers should be unique
      2. js function on click get_posts you should pass the container then as well so when returning posts with AJAX they sit in correct place, eg: get_posts($params, $(‘.closest-container’));

    2. Multiple instances will not work on the same page. Need to make few modifications on the code to enable that

  2. Thanks for sharing all of this. I used your original post on a project about a year ago, and I was excited to find the updated version for my latest endeavor.

    How hard would it be to add a “View All” link that would list all posts with the given taxonomy and not filter anything out?

    1. Hi Procrec, I’m sorry but this is not installable plugin, it is intended for developers use

  3. When i use your example, the user has to push the ‘all-terms’ buttons in order to see all posts right?

    is it possible to set data-term=”all-terms” onLoad?

    1. As said above, it should work if you for example do it with jQuery like: $('a[data-term="all-terms"]').trigger('click');
      Do it inside document.ready .. basically you trigger a click on button instead of user on page load.

  4. Thanks for your answer, however this solution doesn’t work for me. Is all-terms supposed to be replaced by actual terms?

  5. I need this exact feature with checkbox(so multiple category can be selected). Can you help me with that. Thanks in Advance.

    1. Hello,

      Use the shortcode:[ajax_filter_posts per_page=”1″]per_page stands for how many items per page.

  6. Hello Vlado, thank you for this enlighting example.
    However, there’s something I don’t understand in my imlementation: I’m always getting 0 (plain number) as AJAX response (data.msg), and I’m really trying to figure out why.
    The response status code is 200 and the ststausMessage is “success”, so I suppose that the request is correctly processed.
    I’ve tried the same query statically and it returns the correct and expected result, so I suppose that everything in the process is ok, maybe somewhere with the AJAX is wrong? I’ve really got no idea.
    Think you can give me a hint?


    1. I discovered the AJAX request wasn’t correctly processed because I put “`vb_filter_posts()“` inside the template and not into functions.php

      To debug the AJAX request it came to help using the error log: “`error_log( ‘AJAX request check’ );“`

      by defining in “`wp-config.php“`:

      define( ‘WP_DEBUG’, true );
      define( ‘WP_DEBUG_DISPLAY’, true );
      define( ‘WP_DEBUG_LOG’, true );

      and then checking the file “`/wp-content/debug.log“`

    2. Hi Andrea,
      Glad you found your solution. And yes, all of the code should be included in functions.php.
      If you get 0 as AJAX response it always means that function you are calling doesn’t exist.

  7. I have two levels of the taxonomy
    Level 1 (parent)
    sublevel 2 (child)
    I want to have a list of only children taxonomy
    Or drop-down list
    – Level 1
       – sublevel
    How to display only a sub?

  8. Hello

    i have left comment before about infinity scroll :)

    i received email that response from you has been sent but couldn’t find it. :)

    My question was can we implement infinity scroll + can we do some filtering by category + by time in hours :)

    Pozdrav iz Crne Gore :)

  9. Hi

    thanks for your replay!
    I have sent you email if you could please check :)

    I pasted php code from your PHP file gist into functions.php
    copied JS file gist into child theme folder and added following lines at the end
    of functions.php file but on frontend i get only shortcode displayed :(


    1. Hi Jeris,

      It means you didn’t include files well because shortcode doesn’t exist, therefor it cannot be parsed.

    2. Hi! I had this same problem and I spent hours trying to find a solution. At the end it was a simple one. I removed the empty spaces around [] -characters, that can be seen also here: (‘[ajax_filter_posts per_page=”1″]’).

  10. Hi Bobz,

    The code is working perfectly. How do i customize the pagination to use just Previous and Next? without the page numbers?

    Thank you!

  11. Hi Vlado,

    I’m experiencing the same issue as @Nikola and @Jeris where shortcode is only echo and not executed…
    here is my function.php file
    here is how i’m calling the shortcode in my personal category-template.php
    From i clearly understand i’m doing something wrong in the enqueue and or localization but i don’t understand where is my mistake…
    I’ve forked 2016 wordpress built-in theme and hack from here, am i having conflict somewhere ?

    Sorry for the newbies questions, but i would really appreciate some guidance here :D



    1. Hi Matth,

      Sorry for late response. If you just copied it from the above, then maybe the problem is that there is extra space on both sides, it should be
      echo do_shortcode('[ajax_filter_posts per_page="1"]');

  12. First of all, nice job, I’m loving this feature. It made my life easier.

    I’m using it to load a series of posts for a portfolio of one company.
    However, I was wondering whether it’s possible for it to have an extended feature. Is there a possibility of adding a search input field to filter the posts by their title.
    Say we have a load of posts to filter with tags but beside that the client can search their posts via post title.

    1. Hi Arbias,
      Sure it is possible, but it goes beyond this tutorial.
      You need extend js and wp_query part by one field, it’s an extra param.

  13. My friend Thank you for this wonderful explanation. But I would like to ask you how to do it to custom post type because I tried but shortcode doesn’t work>
    Again thank you

    1. This example doesn’t show how to use it with custom post type or taxonomy, but from the source code you should be able to alter the code for your needs.

    2. Hey Mohammmad!
      You need to modifie the Setup query $args: post_type => ‘your_custom_post_type’.
      Cheers: Kolos

    1. Hi Francesco,
      Something not included maybe, please turn debug on you should be able to see and fix your problem.

    1. Hi Razi,
      Something not included maybe, please turn debug on you should be able to see and fix your problem.
      Check php error log too

  14. Hi Vlado, your code is awesome! Really thanks!
    I need to trigger a matchHeight function after ajax was loaded… i’ve found this workaround but sometimes fails… any suggestions?


    $(function() {
    $.fn.matchHeight._maintainScroll = true;

    $(document).ajaxComplete(function() {

    $(‘.display-tour-bottom’).matchHeight(‘remove’).matchHeight({ property: ‘min-height’ });



    1. Hi Jack,
      Maybe call matchHeight in ‘complete‘ part of ‘$.ajax‘ call in my script, should be same or similar like yours.
      Other thing I can think of is maybe trigger window.resize() after eg. 500ms in complete.
      As far I can remember matchHeight triggers after resize too so it might be helpful.

    2. Thanks Vlado,
      it’s works like a charm!

      But (there is always a “but”), as you can see on the below code, I need to call matchHeight() with a delay, I don’t know why, but sometimes matchHeight() don’t detect correctly the height of the divs with inside images. And I’ve this issue only on the first ajax call. It seems that the complete status don’t syncs with images loading.

      Thanks for the help! really love your ninja skills! ;)

      complete: function(data, textStatus) {

      msg = textStatus;

      if (textStatus === ‘success’) {
      msg = data.responseJSON.found;

      $status.text(‘Posts found: ‘ + msg);


      setTimeout( function(){ jQuery(‘.display-tour’).matchHeight(); }, 500);

      jQuery(window).resize(function() {
      setTimeout( function(){ jQuery(‘.display-tour’).matchHeight(‘remove’).matchHeight(); }, 500);


  15. Hey Vlado, thanks so much for the great piece of code :) Just having a small issue where I can’t get it to show all posts on load. I have included “$(‘a[data-term=”all-terms”]’).trigger(‘click’);” when the document is ready but no change.. Any ideas?

    1. Hi Jared,

      Should work, try maybe like: $('a[data-term=all-terms]').trigger(‘click’);.
      Be sure that it exists in markup and it works when you manually try to click on it.

  16. I’m using this method to create an A-Z index, is it possible to set the items to display in alphabetical order, at the moment it is displaying in piublished date order.

    Thank you

    1. Sure it is, just update WP_Query part to sort by title, and terms query to sort by term name.

  17. (I was wrong of page)
    Hi Vlado!, I am trying to exclude terms by id with ‘exclude’ => array(96), but it does not work. (Show all tags)
    function vb_filter_posts_sc($atts) {
    $a = shortcode_atts( array(
    ‘tax’ => ‘tagss’, // Taxonomy
    ‘terms’ => false, // Get specific taxonomy terms only
    ‘exclude’ => array(96),
    ‘active’ => true, // Set active term by ID
    ‘per_page’ => 12 // How many posts per page

    I have also tried including unique id by terms

    function vb_filter_posts_sc($atts) {
    $a = shortcode_atts( array(
    ‘tax’ => ‘tagss’, // Taxonomy
    ‘terms’ => array(’96’), // Get specific taxonomy terms only
    ‘active’ => true, // Set active term by ID
    ‘per_page’ => 12 // How many posts per page

    1. Hi Mark,

      You should check WP Docs for the correct syntax.
      My shortcode doesn’t include option to exclude something unless you extended it yourself.

  18. Hi,

    I have spent over 12 hours now attempting to make this work, and nothing! Clearly I’m doing something wrong.

    All I get in screen is this ‘[ajax_filter_posts per_page=”1″]’

    I have included everything, checked and triple checked.

    1. Shu man, I’m sorry to hear that.
      Sounds like shortcode is not parsing, so maybe filter is removed. Not sure where you adding this, but should be something like: if you are adding shortcode directly into template.

    2. Yep, adding the exact same shortcode to my template file (page-projects.php).

      I have downloaded and copied the php file in to my functions.php file and then uploaded the js file and enqueued it (it’s pulling it in as I have tested that).

      I have tried it with default posts, and a custom post type, and I get the same result which can be seen at (click the down arrow bottom right).

  19. Thank you for this great post. I am having an issue where the returned ajax results are not loading on the page where I have placed the shortcode, instead it is displaying the posts on the index.php template. Any idea why this is happening?

  20. Hi Vlado

    I get an error: TypeError: null is not an object (evaluating ‘data.status’).
    This appears in the $.ajax({}) – Function
    What is this data-variable?

    success: function(data, textStatus, XMLHttpRequest) {
    if (data.status === 200) {
    else if (data.status === 201) {
    else {

  21. Hi, this is really great! I have it working for the most part; however, I need to create a 2 column list of categories (specifically children of 2 categories) that filters posts for a specific custom post type and need some help.

    (1) I’m using WP’s default categories and need to be able to limit the categories and posts shown to the children of specific categories and not a list of all categories/all posts from all categories. How do I do this?

    (2) Is there a way to change it so that it displays checkboxes/uses select functionality?

    You’re help is greatly appreciated!

    1. Sorry for that Bohdan, would you mind to specify URL and how did you managed to trigger error?
      I have checked one here and appears to work correctly.

    2. Hi Kuono, thanks for reporting.
      Looks like it was due to caching, as far as I can see it works now once I cleared the cache.
      Happy coding

  22. Is there a way to add a “reset” button? I’d like have a default loop which is modified by the ajax — and then be able to hit a “reset” button and have the default loop return.

    1. Yes. Check L:25 of vb_filter_posts PHP function.
      There is ‘Show All’ button which is basically a reset, demo.
      So just include data-term="all-terms" in reset link.

  23. When I click between tags I sometimes get the ‘Permission Denied’ response message. It seems to appear if the AJAX request takes too long. Is there any way to stop this from happening?

    1. Be sure to have both of these lines included in your code.
      If you omit ‘wp_ajax_nopriv’, queries will be limited to logged in users only.
      add_action('wp_ajax_do_filter_posts', 'vb_filter_posts');
      add_action('wp_ajax_nopriv_do_filter_posts', 'vb_filter_posts');

  24. Its showing
    Posts found: undefined
    No posts found
    While i am trying to use it for EDD ( Easy Digital Downloads ) products in my site.. I tried the following shortcode [ajax_filter_posts tax="download_category" per_page="2"]

    Its working for the posts but not working for “download_category”. Can you help please?

    1. It’s hard to say.
      Be sure to inspect network activity and see what params you send to admin-ajax.php and see what kind of response you get back.
      This should lead you to your solution.
      Last but not least, check that taxonomy is named correctly and public access is enabled, might be forbidden.


  25. Thanks Vlas, code is working great. Just 2 questions:
    1. I would like to display all posts by default (
    2. I would like to have a show all button (

    I’ve read both comments and I’m using your latest version of the code from gihub but I still won’t show all the posts and the “Show all” button.
    Could you point me in the right direction?
    Thank alot!

    1. 1. I think if you would need to modify a query part a bit like this:
      Then you can use shortcode: [ajax_filter_posts per_page=”1″]
      Because tax and term are not specified, they will return false, it will not include ‘tax_query’ in WP_Query, which should then return everything.

      2. For show all button just use same button without any term/tax specified and it will again not include ‘tax_query’ in WP_Query.
      < a href="#" data-filter="" data-term="" data-page="1" rel="nofollow">
      Show All

  26. Hello,
    first of all, thank you for this very nice tuto.

    I was wondering if it was possible to filter thought a specific category and its subcategories?
    I didn’t find (or understand) how to achieve this.

    Cat1 (dont display this)
    Cat2 (display this)
    -subCat2-1 (display this)
    -subCat2-2 (display this)
    Cat3 (dont display this)

    Thank you & have a nice day!

    1. Hi Samuel,
      If you pass parent category term_id, it will automatically include all child categories.
      So all child categories will be included if you pass only ‘Cat2’ id as per example above.

  27. Hi Vlado,

    This is amazing, thanks so much. I’m trying to implement it on a site at the moment and I’m *so close* but I’m struggling with the pagination.

    I’ve implemented it as per your code, and it loads correctly at the bottom of the grid and shows the right number of pages. Clicking ‘next’ or a specific page number changes the URL of my site from /resources/case-studies to /resources/case-studies#page=2 – however, the results on the page do not change.

    There don’t seem to be any javascript errors in the console when I click on the pagination links, so I’m unsure how to troubleshoot this issue – I was just wondering if you had any ideas?

    If it’s helpful to know, elsewhere on the site I am using non-Ajax pagination which uses the following slug type: /page/2 – however, this is for a different post type than the one I am using the Ajax filterable grid for.

    Many thanks


    1. Hi Hannah,

      I’m glad you are able to make it work.
      If you see ‘#page=2’ in your url, it means that Javascript event ‘on click’ is not binded.
      You need to check and verify that ‘on(‘click’)’ event is set up properly for a ‘next page’ button.

      hope that helps.
      Stay safe

  28. Hiya,

    Sorry for the delayed reply to this! Thanks for the advice, that helped me narrow down the issue – It was my error, I had a different CSS class in the javascript than the one I was using in the pagination. Sorry about that!

    The pagination is now working, but I have the shortcode set to display 8 posts with pagination at the bottom, and when it’s clicked it takes the user to the same place on the next/previous page (i.e. the bottom of the row of 8 posts, or the footer if there are fewer than 8); is there any way to get it to scroll back up to the top of the #container-async area?

    Thanks again!


  29. Heya Bob, this works great!

    My only question is….Is there a way to cause this to show all of a specific category?

    1. Hi Jon, you can increase ‘posts_per_page’, I would use a high number eg 99 instead of -1 which is normally used to show all without limit.

    2. Thanks Bob.

      One last question, and don’t feel obliged to answer as you have been amazing in all your help throughout this thread.

      Is there a way to add a fadein effect through Jquery to fancy it up?

  30. Hi,

    Great code :-)

    Found your code very useful. Thank you for that.

    My question is:
    – How can I view the same filtered list of a certain term by linking it back from a post (which is tagged with the same term). By linking it back, I mean as in a anchor tag in the blog post.

    Best regards

    1. Thank you, I’m glad you found it helpful.
      I would probably pass term in get param, eg:
      Then update vb_filter_posts_sc function to look for $_GET[‘term’] as well when generating initial list.

      Happy coding!

  31. Thank you very much for the filter! Could you let me know, what should I write instead of
    ‘tuts/js’ and ‘bobz’ when you enqueue and localize the javascript file? Because I don’t have these names and paths. Thank you in advance for you help.

    If you can share the folder with the whole website example – would be just amazing!

    Best regards,

  32. Hello, very useful code.

    Is there a way to change it so that it uses select (select option) functionality insted of list (ul li) to display terms?
    Thank you

    1. Hi Carl, yes it is but customising is up to you. This tutorial should give you know how, I’m not able to provide custom solutions for every possible use case.

  33. Great solution! still going strong in 2021!

    When it first loads it shows all the posts which perfect but are we able to term above each section? so for example if we using a b c d as filters for the posts I would like to show A above all a’s post and B above all B’s post on the first load ( show all section ) ?

  34. This has saved me from 4 days of headache, thank you so much!

    I’m curious if it’s possible to entirely ignore filtering by terms and only filter by category instead? I’m attempting to build a grid of 9 posts per page with pagination, and there is a nav bar on top allowing you to choose which category to filter from. Term filtering isn’t required at all. Is it possible to re-engineer this code to do something like that?

    1. Hi Matt, surely it’s possible, but you’ll need to tweak code a bit to make that work.
      There are too many possibilities how this can be used so the purpose of this article is to teach readers how to do it on their own and not to solve every specific request.
      Happy coding!

  35. Hello, I managed to implement this with custom post type successfully. But now, client wants 2 different post types to have this used on two different pages. I tried copying code and changing every function name in functions.php but it always pulls posts from first one. I used custom post type by changing “‘post_type’ => ‘post’,”.

    How can I use this code for two post types?

    Thank you!

    1. Sounds like you might need to add another function to wp_ajax, it’s like your JS is always calling same ‘do_filter_posts’ action.
      add_action('wp_ajax_do_filter_posts', 'vb_filter_posts');
      add_action('wp_ajax_nopriv_do_filter_posts', 'vb_filter_posts');

      Might be better in your case to extend params to include $post_type as well, follow what’s done for quantity and paging.

  36. Hey in console showing this error
    SyntaxError: Unexpected token < in JSON at position 0
    at parse ()
    at jquery-3.6.0.min.js:2:79470
    at l (jquery-3.6.0.min.js:2:79587)
    at XMLHttpRequest. (jquery-3.6.0.min.js:2:82355)

    and frontend
    Posts found: parsererror

    1. I think I have the same problem. Status is parsererror and it’s probably because of “all-terms” in $params. Have you solved it?

  37. Hi,

    first to mention, that this is a great tutorial you made here! Awesome!!! :D

    I have only one (simple?) problem:

    The “active=” parameter from the shortcode is working, as it highlights the current filter link. But it doesn’t show the posts belonging to that link when the page first loads. I have to click it first, to see the posts. Is there an easy way to show posts on page load too?

    best regards,

Leave a Reply

Your email address will not be published. Required fields are marked *

Please don’t paste any HTML, Js or PHP code into comments box, use or similar service to share code examples.

Characters left: 1000