How to Create AJAX Filters

In this tutorial I am going to guide you step by step through the process of creating WordPress AJAX filters and I am about to show you two examples – at the first one we will create a simple post filter by category and in the second example we will dive deep into WP_Query in order to create a complex search filter with a couple of custom fields.

In our examples we’re also going to use:

Let’s get straight to the AJAX filter examples now.

Example 1. Filter Posts by Category (without a Plugin)

Below is the preview of how our AJAX category filter is going to look like:

Filter posts by category in WordPress without plugins
Isn’t it amazing?

Now let’s break the process of creating this AJAX category filter down into a step by step.

1.1 – Display categories select dropdown in HTML

Our first goal here is to create some HTML for the filter and then to decide where to insert it into the WordPress theme which 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.

1.2 – 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 admin-ajax.php 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( { 
			'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 a jQuery filter implementation, I think a little bit later there is going to be a WooCommerce tutorial about that as well.

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, {

1.3 – Get posts filtered by category inside wp_ajax_ hook

The last but not the least – the part which goes into 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:

Example 2. Posts Search Filter (without a Plugin)

Well, it is time to make our AJAX filter a little bit more interesting. By “interesting” I mean:

Less words, more screenshots! Here we go:

AJAX search filter in WordPress example

Let’s update our HTML form now:

<div class="ajax-filters">
	<form id="ajax-filter">
		<input type="text" name="s" placeholder="Seach by title..." />
		<select name="city">
			<option value="">Select city...</option>
			<option value="athens">Athens</option>
			<option value="milano">Milan</option>
			<option value="copenhagen">Copenhagen</option>
		</select>
		<label>
			<input type="checkbox" name="image" /> With images only
		</label>
		<button class="wp-block-search__button wp-element-button">Filter</button>
	</form>
</div>

So I’ve just added a couple of fields and a button to our AJAX filter <form> element. And we don’t even have dynamic fields (printed with PHP) here, well, we can make “City” field dynamic by using a SELECT DISTINCT meta_value from $wpdb->postmeta query but it is an unnecessary complication for this tutorial I think.

Next step – JavaScript.

const ajaxFilter = document.getElementById( 'ajax-filter' )
const siteContent = document.getElementById( 'site-content' )

ajaxFilter.addEventListener( 'submit', event => {
	
	event.preventDefault();

	siteContent.classList.add( 'is-loading' )

	const formData = new FormData( event.target ) // similar to jQuery's serialize()

	fetch( ajaxurl, {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json'
		},
		body: JSON.stringify( Object.fromEntries( formData.entries() ) ),
	}).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 )
	})

} )

Two main moments I would like to highlight here:

And the last but not least, processing the AJAX request WordPress-way:

add_action( 'wp_ajax_ajaxfilter2', 'rudr_ajax_search_filter' );
add_action( 'wp_ajax_nopriv_ajaxfilter2', 'rudr_ajax_search_filter' );

function rudr_ajax_search_filter() {

	$form_data = json_decode( file_get_contents( "php://input" ), true );

	// some default args go here
	$args = array(
		'posts_per_page' => -1,
		'meta_query' => array(
			'relation' => 'AND'
		),
	);
	// check search field
	if( isset( $form_data[ 's' ] ) && $form_data[ 's' ] ) {
		$args[ 's' ] = $form_data[ 's' ];
	}
	// check "city" custom field
	if( isset( $form_data[ 'city' ] ) && $form_data[ 'city' ] ) {
		$args[ 'meta_query' ][] = array(
			'key' => '_city',
			'value' => $form_data[ 'city' ],
		);
	}
	// check if post has a featured image set
	if( isset( $form_data[ 'image' ] ) && 'on' === $form_data[ 'image' ] ) {
		$args[ 'meta_query' ][] = array(
			'key' => '_thumbnail_id',
			'compare' => 'EXISTS'
		);
	}
	query_posts( $args );

	$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;

}

In the code above I am using meta_query which is not a very complex one, but if you need a more detailed explanation how it works, check this tutorial.

Misha Rudrastyh

Misha Rudrastyh

Hey guys and welcome to my website. For more than 10 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

Follow me on X