Autocomplete Search Pop-up for WordPress

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.

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.

Start with some Scaffolding

infoBefore starting, make sure that you’re using a custom child theme.

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.


 * Custom AJAX Search (CAS)

// Block direct access.
if (!defined('WPINC')) {
	exit('Do NOT access this file directly.');

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:

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


 * Custom AJAX Search (CAS)

// Block direct access.
if (!defined('WPINC')) {
	exit('Do NOT access this file directly.');

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

 * 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() {
	$base_url = get_stylesheet_directory_uri() . '/' . pathinfo(__FILE__, PATHINFO_FILENAME) . '/';
	$theme_version = wp_get_theme()->get('Version');

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

	// Enqueue and "localise" our script in the page footer.
		$base_url . 'cas-frontend.js',
		'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');

 * Our main search function.
function cas_search() {
	$response = array();
	$search_query = wp_strip_all_tags($_POST['searchQuery']);
	// error_log('search: ' . $search_query);

	if (!empty($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()) {

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

		// Usually you would call wp_reset_postdata() here, but we don't need
		// to because our code is the last thing that's going to happen - we're
		// calling wp_die().

	echo json_encode($response);

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 (e.g. Comet Cache). 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.css

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 rgba(0, 0, 0, 0.2);
    box-shadow: 0 0 0.25em rgba(0, 0, 0, 0.2);

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

.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
(function($) {
    'use strict';

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

        if (typeof(casData) != 'undefined') {
                appendTo: $("form"),
                minLength: 3,
                source: customSearchSource,
                select: searchSelectedItem

            function searchSelectedItem(event, ui) {
                // Copy the slected item into the search text box.

                // Find the clostest (parent) form and submit it.

            function customSearchSource(request, response) {
                // console.log('search: ' + request.term);

                // Send the HTTP POST data to our server, with the action and
                // search term (searchQuery).
                $.post(casData.ajaxUrl, {
                        action: casData.ajaxAction,
                        searchQuery: request.term
                    function(data, status) {
                        var responseData = null;

                        if (data) {
                            // console.log('good response: ' + data);
                            var responseData = JSON.parse(data);
                        } else {
                            // console.log('bad response: ' + data);

                        if (Array.isArray(responseData)) {
                        } else {

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 🔍

Leave a Comment

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