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
Table of Contents
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');PHPWhy This Matters:
show_in_restenables Gutenberg and API supporthas_archiveallows archive pagessupportsensures content flexibilityrewritekeeps 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);PHPWhy 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;
}PHPadd_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;
}
}PHPAdmins 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 */PHPWe 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,
));PHPStep 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(); ?>PHPWhy 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>PHPThis 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');PHPThis 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');
PHPWhy 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 TypesKey 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.



