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?

  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.

    1. Hello,

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

  4. 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.

  5. 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?

  6. 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 :)

  7. 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″]’).

  8. 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!

  9. 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"]');

  10. 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.

  11. 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

  12. 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);


  13. 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.

  14. 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.

  15. (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.

  16. 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: < ?php echo do_shortcode('[ajax_filter_posts per_page="1"]'); ?> 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).

  17. 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?

  18. 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 {

  19. 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!

Leave a Reply

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