WordPress AJAX Category Filter for Custom Post Types

Learn how to implement a WordPress AJAX Category Filter for Custom Post Types to dynamically filter content by categories without page reloads. Step-by-step guide with clean, production-ready code for better user experience and performance.

Modern WordPress websites are no longer just about displaying content. Users expect speed, interactivity, and smooth navigation. One of the most effective ways to improve user experience is by loading and filtering content dynamically using AJAX.

In this article, we will build a WordPress AJAX Category Filter for Custom Post Types using a real-world, production-ready implementation. This solution allows users to filter posts by category and load additional posts without reloading the page.

This guide is written from the perspective of a WordPress backend developer and is suitable for developers who want a clean, scalable, and reusable WordPress AJAX Category Filter for Custom Post Types

What You Will Learn in This Guide

By the end of this tutorial, you will understand how to:

  • Create a Custom Post Type in WordPress
  • Register a Custom Taxonomy for filtering
  • Display category tabs dynamically
  • Load posts using AJAX without page refresh
  • Implement a pagination-like Load More system
  • Improve admin usability with custom columns
  • Structure AJAX logic cleanly for scalability

All code used here is practical, tested, and suitable for real projects.

Why Use WordPress AJAX Category Filter for Custom Post Types?

AJAX filtering is not just about looks; it has real benefits:

  • Faster page interaction
  • No full page reloads
  • Better user engagement
  • Improved Core Web Vitals
  • Cleaner UI for content-heavy sites

This approach is especially useful for:

  • Custom content websites
  • Knowledge bases
  • Listing-based platforms
  • Portfolio and service sites

Implementation of the WordPress AJAX Category Filter for Custom Post Types

We will use the following components:

  • Custom Post Type: recipes (can be reused for any content)
  • Custom Taxonomy: recipe_category
  • Custom Page Template
  • AJAX Handlers (PHP)
  • jQuery AJAX Calls
  • Admin Column Enhancements

Even though the post type is named recipes, This implementation works for any custom post type.

Step 1: Registering the Custom Post Type

We start by registering a custom post type. This keeps our content separate from blog posts and gives us more flexibility.

function recipes_post_type()
{
    register_post_type(
        'recipes',
        array(
            'labels' => array(
                'name' => __('Recipes'),
                'singular_name' => __('Recipe')
            ),
            'public'        => true,
            'show_in_rest'  => true,
            'has_archive'   => true,
            'rewrite'       => array('slug' => 'recipes'),
            'supports'      => array('title', 'editor', 'thumbnail'),

        )
    );
}
add_action('init', 'recipes_post_type');
PHP

Why This Matters:

  • show_in_rest enables Gutenberg and API support
  • has_archive allows archive pages
  • supports ensures content flexibility
  • rewrite keeps URLs clean

This setup follows WordPress best practices.

Step 2: Creating a Custom Taxonomy for Filtering

Next, we register a hierarchical taxonomy to categorize posts.

function wpdocs_create_book_tax_rewrite()
{
    $labels = array(
        'name' => 'Recipes Categories',
    );
    $args = array(
        'label' => 'Recipes Categories',
        'labels' => $labels,
        'public' => true,
        'hierarchical' => true,
        'show_ui' => true,
        'show_in_nav_menus' => true,
        'rewrite' => array(
            'slug' => 'recipes/recipe-category',
            'with_front' => true,
            'hierarchical' => true
        ),
    );

    register_taxonomy('recipe_category', 'recipes', $args);
}
add_action('init', 'wpdocs_create_book_tax_rewrite', 0);
PHP

Why Hierarchical Taxonomy?

  • Supports parent-child categories
  • Improves organization
  • Makes filtering more intuitive
  • Better for long-term scalability

Step 3: Enhancing the Admin Panel with Custom Columns

A professional solution must also improve the admin experience.

add_filter('manage_edit-recipes_columns', 'set_columns');
function set_columns($columns)
{
    $columns['recipe_category'] = 'Category';
    $columns['recipe_thumnail'] = 'Thumbnail';
    return $columns;
}
PHP
add_action('manage_recipes_posts_custom_column', 'fill_columns');
function fill_columns($column)
{
    global $post;

    switch ($column) {

        case 'recipe_category':
            $terms = get_the_terms($post->ID, 'recipe_category');
            if (!empty($terms) && !is_wp_error($terms)) {
                foreach ($terms as $term) {
                    echo '<p class="cat">' . esc_html($term->name) . '</p>';
                }
            } else {
                echo '—';
            }
            break;

        case 'recipe_thumnail':
            if (has_post_thumbnail($post->ID)) {
                echo get_the_post_thumbnail($post->ID, 'thumbnail');
            } else {
                echo '—';
            }
            break;
    }
}
PHP

Admins can instantly see:

  • Assigned categories
  • Featured images
  • Content quality at a glance

This improves workflow efficiency.

Step 4: Creating the Custom Page Template

Now we build the front-end template that displays categories and posts.

/*Template Name: Recipe */
PHP

We fetch:

  • Initial posts
  • Available categories
$posts_per_page = 1;
$query = new WP_Query(array(
    'post_type' => 'recipes',
    'posts_per_page' => 1,

));
$count = wp_count_posts();
$total_posts = $count->publish;
PHP
$category_list = get_terms(array(
    'taxonomy' => 'recipe_category',
    'hide_empty' => true,
));
PHP

Step 5: Displaying Category Tabs Dynamically

<?php

/*Template Name: Recipe 
*/

get_header();

$posts_per_page = 1;
$query = new WP_Query(array(
    'post_type' => 'recipes',
    'posts_per_page' => 1,

));
$count = wp_count_posts();
$total_posts = $count->publish;
$category_list = get_terms(array(
    'taxonomy' => 'recipe_category',
    'hide_empty' => true,
));
?> 
<div class="recipes-post-wrapper">
    <div class="container">
        <div class="recipe-category">
            <?php if (!empty($category_list)) : ?>
                <div class="recipe-cat-item active" data-id="<?php echo esc_attr(1); ?>"> //you can write data-id="all"
                    <?php echo "View All"; ?>
                </div>
                <?php foreach ($category_list as $cat) : ?>
                    <div class="recipe-cat-item" data-id="<?php echo $cat->term_id; ?>">
                        <?php echo esc_html($cat->name); ?>
                    </div>
                <?php endforeach; ?>
            <?php endif; ?>
        </div>

        <div class="recipes-grid">
            <?php if ($query->have_posts()) :
                while ($query->have_posts()) : $query->the_post(); ?>

                    <div class="recipes-item">
                        <h3 class="recipes-title"><?php the_title(); ?></h3>
                        <p class="recipes-excerpt"><?php echo wp_trim_words(get_the_excerpt(), 20); ?></p>
                        <a href="<?php the_permalink(); ?>" class="recipes-readmore">Read More</a>
                    </div>

            <?php endwhile;
            endif; ?>

        </div>
        <div class="load-more-btn-recipe">
            <button type="button" data-page=1>Load More</button>
        </div>
    </div>
</div>

<?php get_footer(); ?>
PHP

Why this works well:

  • Categories load dynamically
  • No hardcoded values
  • Easy to style
  • Works with AJAX seamlessly

Step 6: Initial Content Display

<div class="recipes-grid">
    <?php while ($query->have_posts()) : $query->the_post(); ?>
        <div class="recipes-item">
            <h3 class="recipes-title"><?php the_title(); ?></h3>
            <p class="recipes-excerpt">
                <?php echo wp_trim_words(get_the_excerpt(), 20); ?>
            </p>
            <a href="<?php the_permalink(); ?>" class="recipes-readmore">Read More</a>
        </div>
    <?php endwhile; ?>
</div>
PHP

This ensures:

  • Fast first paint
  • SEO-friendly HTML
  • Accessible content

Do you know how to use Ajax in WordPress? Read here

Step 7: AJAX Handler for Category Filtering

The following function loads filtered posts based on the selected category.

function tabbing_recipes()
{

    $cat_id = intval($_POST['cat_id']);
    $args = array(
        'post_type' => 'recipes',
        'posts_per_page' => 1,

        'tax_query' => array(
            array(
                'taxonomy' => 'recipe_category',
                'field'    => 'id',
                'terms'    => $cat_id,

            ),
        ),
    );
    $wp_query = new WP_Query($args);
    $args1 = array(
        'post_type' => 'recipes',
        'posts_per_page' => 1,
    );
    $view = new WP_Query($args1);

    
// OR
$args = array(
        'post_type'      => 'testimonial',
        'post_status'    => 'publish',
        'posts_per_page' => 5, //acc to your requirement
        'orderby'        => 'date',
        'order'          => 'DESC',
    );

    if ($cat_id !== 'all') {
        $args['tax_query'] = array(
            array(
                'taxonomy' => 'testimonials-category',
                'field'    => 'term_id',
                'terms'    => (int) $cat_id,
            ),
        );
    }

    $query = new WP_Query($args); // for the tabbing like if you use data-id as all then you can do this

    ob_start();
    if ($wp_query->have_posts() && $cat_id != 1) :
        while ($wp_query->have_posts()) : $wp_query->the_post(); ?>

            <div class="recipes-item">
                <h3 class="recipes-title"><?php the_title(); ?></h3>
                <p class="recipes-excerpt"><?php echo wp_trim_words(get_the_excerpt(), 20); ?></p>
                <a href="<?php the_permalink(); ?>" class="recipes-readmore">Read More</a>
            </div>

        <?php endwhile;
    else:
        while ($view->have_posts()) : $view->the_post(); ?>

            <div class="recipes-item">
                <h3 class="recipes-title"><?php the_title(); ?></h3>
                <p class="recipes-excerpt"><?php echo wp_trim_words(get_the_excerpt(), 20); ?></p>
                <a href="<?php the_permalink(); ?>" class="recipes-readmore">Read More</a>
            </div>
        <?php endwhile;
    endif;
    $html = ob_get_clean();
    wp_send_json_success(['html' => $html]);
}
add_action('wp_ajax_tabbing_recipes', 'tabbing_recipes');
add_action('wp_ajax_nopriv_tabbing_recipes', 'tabbing_recipes');
PHP

This function:

  • Receives category ID
  • Runs a WP_Query
  • Returns HTML
  • Updates content without a reload

This approach is secure, fast, and scalable.

Step 8: AJAX Handler for Dynamic Pagination

function load_with_tabbing()
{
    $term_id = intval($_POST['term_id']);
    $posts_per_page = 1;
    $page = isset($_POST['page']) ? intval($_POST['page']) : 1;
    $is_view_all = ($term_id == 1);
    $args = array(
        'post_type' => 'recipes',
        'posts_per_page' => -1,
        'tax_query' => array(
            array(
                'taxonomy' => 'recipe_category',
                'field'    => 'term_id',
                'terms'    => $term_id,
            ),
        ),
    );

    if ($is_view_all) {

        $all_query = new WP_Query(array(
            'post_type' => 'recipes',
            'posts_per_page' => -1,
        ));
        $all_posts = $all_query->posts;
    } else {

        $wp_query = new WP_Query($args);
        $all_posts = $wp_query->posts;
    }

    $start = ($page - 1) * $posts_per_page;
    $sliced = array_slice($all_posts, $start, $posts_per_page);

    ob_start();
    foreach ($sliced as $recipe) { ?>
        <div class="recipes-item">
            <h3 class="recipes-title"><?php echo get_the_title($recipe); ?></h3>
            <p class="recipes-excerpt"><?php echo wp_trim_words(get_the_excerpt($recipe), 20); ?></p>
            <a href="<?php echo get_permalink($recipe); ?>" class="blog-readmore">Read More</a>
        </div>
    <?php }
    $has_more = ($start + $posts_per_page) < count($all_posts);

    $html = ob_get_clean();
    wp_send_json_success([
        'html' => $html,
        'has_more' => $has_more
    ]);
}

add_action('wp_ajax_load_with_tabbing', 'load_with_tabbing');
add_action('wp_ajax_nopriv_load_with_tabbing', 'load_with_tabbing');
PHP

Why This Approach Is Better:

  • Avoids traditional pagination reloads
  • Keeps UI clean
  • Works with category filtering
  • Supports unlimited content

Read this: How to Load More Posts with AJAX Load More in WordPress

Step 9: Frontend jQuery Logic

Your JavaScript connects the UI with backend logic.

  jQuery(".recipe-cat-item").on("click", function () {
    jQuery(this)
      .parent(".recipe-category")
      .siblings(".load-more-btn-recipe")
      .find(" button")
      .attr("data-page", 2);
    jQuery(this).siblings().removeClass("active");
    jQuery(this).addClass("active");
    var cat_id = jQuery(this).attr("data-id");
    $.ajax({
      url: learning_custom_script_object.ajaxurl,
      type: "POST",
      data: {
        cat_id: cat_id,
        action: "tabbing_recipes",
      },
      success: function (response) {
        if (response.data.html) {
          jQuery(".recipes-grid").html(response.data.html);
        }
      },
    });
  });
  jQuery(".load-more-btn-recipe button").on("click", function () {
    var term_id = jQuery(this)
      .parent(".load-more-btn-recipe")
      .siblings(".recipe-category")
      .find(".active")
      .attr("data-id");
    var page = parseInt(jQuery(this).attr("data-page"));
    $.ajax({
      url: learning_custom_script_object.ajaxurl,
      type: "POST",
      data: {
        term_id: term_id,
        page: page,
        action: "load_with_tabbing",
      },
      success: function (response) {
        if (response.data.html) {
          jQuery(".recipes-grid").append(response.data.html);
          $(".load-more-btn-recipe button").attr("data-page", page + 1);

          if (!response.data.has_more) {
            $(".load-more-btn-recipe button").hide();
          }
        }
      },
    });
  });
WordPress AJAX Category Filter for Custom Post Types

Key UX Benefits

  • Smooth transitions
  • No page flicker
  • Clear user feedback
  • Mobile-friendly interaction

Performance & SEO Considerations

This AJAX-based filtering approach improves user experience while keeping performance and SEO intact. The initial page load stays lightweight, and additional content is fetched only when needed, reducing unnecessary server load and improving responsiveness across devices.

Key benefits:

  • Faster interactions by avoiding full-page reloads
  • Optimized queries using WordPress-native functions
  • Clean URLs and server-rendered HTML for better crawlability
  • Improved engagement signals like time on page and reduced bounce rate

This balance ensures the feature remains both performance-efficient and search-engine friendly for production websites.

For deeper technical understanding, refer to the official WordPress documentation: https://developer.wordpress.org/plugins/javascript/ajax/

Final Thoughts

The WordPress AJAX Category Filter for Custom Post Types is a practical and production-ready solution that enhances both user experience and site performance. By combining custom post types, taxonomies, and AJAX handling, this approach allows visitors to filter content dynamically without page reloads, making navigation smooth and intuitive.

From a developer’s perspective, this setup is clean, scalable, and easy to maintain. New categories, additional content, or design updates can be implemented without rewriting the core logic. The separation of PHP and JavaScript responsibilities ensures that debugging and extending the feature remains straightforward, even as your website grows.

Leave a Comment

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.