Autocomplete Search Pop-up for WordPress

magnifying glass

Learn how to extend the search box on your WordPress website with an autocomplete/suggestions pop-up. We’ll create some PHP code to do the back-end search, and use jQuery to convert the search text box into an autocomplete pop-up. WordPress AJAX handlers will pick-up the incoming search requests for us, and we’ll use a PHP function to return a simple array of suggested strings.

importantBefore starting, make sure that you’re using a custom child theme so you can edit functions.php.

How It’ll Work

We’re going to need two major components here…

  1. A front-end JavaScript component to deal with the user’s text input and display the drop-down search suggestions.
  2. A back-end PHP component to do the actual search – search the database for the user’s requested search phrase.
    • This’ll be a simple function that uses WP_Query to run a standard WordPress loop.

We’ll start by creating some placeholder files and link the main PHP file to our custom child theme’s funcitons.php. Then we’ll smash together the back-end PHP code. Finally, we’ll get the front-end JavaScript bit working and test it all.

Scaffold the Code

The first thing to do is create some placeholder files to hold our code. Create a PHP file in your custom child theme’s main folder called custom-ajax-search.php and paste the following into it. We’ll replace this later – this is just to get things started.

<?php

/**
 * Custom AJAX Search (CAS)
 *
 * https://wp-tutorials.tech/refine-wordpress/autocomplete-search-wordpress/
 */

// Block direct access.
defined('WPINC') || die();

Next, create a subfolder (again, in your custom child theme’s main folder) called custom-ajax-search, and create two empty files in there called cas-frontend.css and cas-frontend.js, so you end up with something like this:

custom ajax search assets
Placeholder files for our font-end assets.

Now we need to “require” our main PHP file from the custom child theme, so open functions.php and add the following code to it:

/**
 * Custom AJAX Search (CAS) - autocomplete
 */
require_once dirname(__FILE__) . '/custom-ajax-search.php';

With the scaffolding in-place, create some content on your WordPress site that’s got a site search box on it. On this tutorial page, there are two… one is global (in the site’s primary menu) and the other is a standard Gutenberg Search Block.

Right, assuming the above hasn’t broken your site somehow, we can get on with the actual code.

The Back-End PHP Bit

The back-end PHP code needs to do two major jobs for us…

  1. Initialise our code components:
    1. Set up our WordPress action/hook handlers.
    2. Add the relevant JavaScript and CSS to the page.
  2. Handle incoming AJAX requests:
    1. Search the website content.
    2. Return a simple array of strings in JSON format.

The code to do all this is surprisingly short. Just copy-and-paste the following into custom-ajax-search.php, replacing the scaffolding code we had in there from earlier.

<?php

/**
 * Custom AJAX Search (CAS)
 *
 * https://wp-tutorials.tech/refine-wordpress/autocomplete-search-wordpress/
 */

// Block direct access.
defined('WPINC') || die();

// Configure with these settings.
const CAS_ACTION_SEARCH = 'cas_search';
const CAS_DEFAULt_CSS_SELECTOR = 'input[type="search"]';
const CAS_MAX_SEARCH_RESULT_COUNT = 5;

/*
 * Listen for incoming AJAX requests.
 */
function cas_init() {
	add_action('wp_ajax_nopriv_' . CAS_ACTION_SEARCH, 'cas_search');
	add_action('wp_ajax_' . CAS_ACTION_SEARCH, 'cas_search');
}
add_action('init', 'cas_init');

/**
 * Add our CSS and JS to the pages.
 */
function cas_enqueue_scripts() {
	$theme_version = WPT_ADDONS_VERSION;
	$base_url = plugin_dir_url(__FILE__) . '/custom-ajax-search/';

	// $base_url = get_stylesheet_directory_uri() . '/custom-ajax-search/';;
	// $theme_version = wp_get_theme()->get('Version');

	$handle = 'cas-frontend';

	// Add our stylesheet.
	wp_enqueue_style(
		$handle,
		$base_url . 'cas-frontend.css',
		null, // No style dependencies
		$theme_version
	);

	// Enqueue and "localise" our script in the page footer.
	wp_enqueue_script(
		$handle,
		$base_url . 'cas-frontend.js',
		array('jquery-ui-autocomplete'),
		$theme_version,
		true
	);
	wp_localize_script(
		$handle,
		'casData', array(
			'cssInputSelectors' => CAS_DEFAULt_CSS_SELECTOR,
			'ajaxAction' => CAS_ACTION_SEARCH,
			'ajaxUrl' => admin_url('admin-ajax.php'),
		)
	);
}
add_action('wp_enqueue_scripts', 'cas_enqueue_scripts');

/**
 * This AJAX-handler is our core search function.
 */
function cas_search() {
	$response = array();
	$search_query = wp_strip_all_tags($_POST['searchQuery']);

	if (empty($search_query)) {
		error_log(__FUNCTION__ . ' : No search term specified');
	} else {
		// error_log('search: ' . $search_query);

		$args = array(
			'post_type' => 'post',
			'post_status' => 'publish',
			's' => $search_query,
			'posts_per_page' => CAS_MAX_SEARCH_RESULT_COUNT,
		);

		$query = new WP_Query($args);
		while ($query->have_posts()) {
			$query->the_post();

			$response[] = html_entity_decode(get_the_title());
		}

		// Usually we would call wp_reset_postdata() here, but we don't need
		// to because our code is the last thing that's going to happen.
		// The wp_send_json() function terminates execution after sending the
		// response to the browser.
	}

	// HTTP_RESPONSE 200 means OK
	wp_send_json($response, 200);
}

The code should be easy to read, and follows a pretty standard structure for this sort of AJAX-based request-and-response sequence.

The core of the search functionality hangs off a custom AJAX action that we’ve defined, called “cas_search” (defined by CAS_ACTION_SEARCH). The actual actions that WordPress will raise are wp_ajax_nopriv_cas_search (if the user is not logged-in) and wp_ajax_cas_search (if the user is logged-in).

importantUsually you’d want to include a WordPress nonce in your request-response, so you can verify the incoming user requests. But… a nonce is session-specific, so each visitor should be assigned their own nonce. Our search box is most definitely public-facing and your website will probably be using some sort of page-caching plugin. You should never include a nonce on a cached page, because once the page is cached, every visitor who sees the page will get the same nonce.

Some Necessary Styles

The jQuery UI Autocomplete library (in WordPress) doesn’t provide its own styling to handle things like the pop-up suggestions-list. So… we need to do this ourselves. It’s pretty simple CSS and it means we’ve got 100% control over how our pop-up renders. Paste the following into your cas-frontend.css file to get started.

/*
 * cas-frontend.js
 *
 * https://wp-tutorials.tech/refine-wordpress/autocomplete-search-wordpress/
 */

form[role="search"] {
	position: relative;
	padding: 0;
	margin: 0;
}

.ui-autocomplete {
	position: absolute;
	z-index: 1000;
	float: left;
	display: none;
	min-width: 8em;
	list-style: none;
	background-color: white;
	border: 1px solid #888;
	border-top:  none;
	box-shadow: 0 0 0.5rem #0004;
}

.ui-autocomplete .ui-menu-item-wrapper {
	display: block;
	padding: 0.5em 1em;
}

.ui-state-hover,
.ui-state-active {
	color: white;
	text-decoration: none;
	border: none;
	cursor: pointer;

	/* You probably want to change the background-color */
	background-color: blue;
}

.ui-helper-hidden-accessible {
	display: none;
}

The Front-End Browser Bit – The JavaScript

This final section, the front-end browser-based JavaScript code, would be difficult and time-consuming to write from scratch. So we’re not going to do that. In the PHP code, notice that our cas-frontend.js script depends upon jquery-ui-autocomplete, which is a standard script included with WordPress. It’s an easy-to-use way of adding pop-up autocomplete boxes.

Copy-and-paste this lump into cas-frontend.js.

/*
 * cas-frontend.js
 *
 * https://wp-tutorials.tech/refine-wordpress/autocomplete-search-wordpress/
 */
(function($) {
	'use strict';

	$(window).on('load', function() {
		// Diagnostics
		console.log('custom-ajax-search : load');

		if (typeof(casData) != 'undefined') {
			// Diagnostics
			console.log('custom-ajax-search : init');

			$(casData.cssInputSelectors).autocomplete({
				appendTo: $(event.target).closest("form"),
				minLength: 3,
				source: customSearchSource,
				select: searchSelectedItem
			});

			function searchSelectedItem(event, ui) {
				// Diagnostics
				console.log(`Search now: ${ui.item.label}`);

				// Copy the slected item into the search text box.
				$(event.target).val(ui.item.label);

				// Find the clostest (parent) form and submit it.
				$(event.target).closest("form").submit();
			}

			function customSearchSource(request, response) {
				console.log(`search: ${request.term}`);

				// This is the object we want to send to our AJAX handler.
				let requestData = {
					action: casData.ajaxAction,
					searchQuery: request.term
				};

				// Diagnostics
				console.log(requestData);

				// Send the HTTP POST data to our server, with the action and
				// search term (searchQuery).
				$.post(casData.ajaxUrl, requestData)
					.done((responseData) => {
						// Diagnostics
						console.log('done()');
						console.log(responseData);

						if (Array.isArray(responseData)) {
							response(responseData);
						} else {
							response([]);
						}
					})
					.always(() => {
						// Diagnostics
						console.log('always()');

						// If there is any code that needs running after the search
						// results have been returned (like hiding a spinner),
						// you can do that in here.
						// ...
					});

			}
		}
	});
})(jQuery);

Notice the commented-out lines with calls to console.log(...). If you uncomment these you’ll see useful diagnostic messages in your browser’s JavaScript console. Just remember to comment them out again (or delete them) when you’re finished… to keep things nice and tidy.

The logic breaks down like this:

  • After the document has loaded (the ‘load’ event)…
    • If the global variable casData has been set (using the wp_localize_script() function in our PHP file)…
      • For all the matching CSS selectors (‘form[role=”search”] input[type=”search”]’), call the jQuery-UI function, autocomplete()
        • “source” handler: customSearchSource: This uses the jQuery post() function to call our back-end cas_search AJAX action and return the results as a simple array of strings.
        • “select” handler: searchSelectedItem: Called when the user selects an item in the autocomplete pop-up list. We copy the selected item into our search text box and then we submit the form.

That should do it. Have a play with it and enjoy the satisfaction.

Tweak and Extend

Extending and customising this should be pretty easy, because we’ve kept the core logic in a nice standalone little function… cas_search(). All this function needs to do is populate the $response array with relevant/suggested strings, echo the array in JSON format, and then call wp_die().

You could modify $args so it only searches post titles, or maybe change the post_type to something more suited to your site, like a custom post type. You could also switch from searching posts to searching taxonomy terms… anything you want. Just return an array of strings in $response. Easy.

Happy site-searching 🔍👍

Like This Tutorial?

Let us know

WordPress plugins for developers

Leave a comment