How to add autocomplete to the search with datalist and JS in WordPress

Contents

Introduction

This tutorial shows step-by-step how to add autocomplete to your WordPress search using the native HTML datalist element and JavaScript. It covers two approaches (simple static datalist for very small sites and the recommended dynamic approach using the WordPress REST API), plus an admin-ajax alternative, performance and security considerations, accessibility tips, and complete code examples you can drop into a themes functions.php and JS file.

Why use datalist JavaScript?

  • datalist is a lightweight native browser feature that shows suggested values for an input field.
  • JavaScript can populate the datalist dynamically from your WordPress content (post titles, taxonomies, custom fields), making it scalable and responsive.
  • Compared with full-blown autocomplete libraries, datalist requires less markup and fewer dependencies.

What youll get

  • Search input with datalist UI that shows matching post titles as the user types.
  • REST API endpoint in WordPress to return JSON suggestions (fast and cacheable).
  • JavaScript with debounce and minimum character limit to avoid excessive requests.
  • Security and performance recommendations.

Prerequisites

  • WordPress 4.7 (REST API integration is recommended).
  • Ability to edit your themes functions.php or add a small plugin.
  • Basic familiarity with enqueueing scripts and editing a search form template (searchform.php) or using get_search_form filter.

Approach A — Very small sites: Static datalist embedded in the page

Use this only if you have a very small number of items (e.g., <= 200 titles) and can include them inline without performance issues.

Search form markup (example)

ltform role=search method=get id=searchform action=lt?php echo esc_url( home_url( / ) ) ?gtgt
  ltlabel for=s class=screen-reader-textgtSearch for:lt/labelgt
  ltinput type=search id=s name=s list=search-suggestions autocomplete=off placeholder=Search… /gt
  ltdatalist id=search-suggestionsgt
    lt!-- Options populated server-side example below shows three --gt
    ltoption value=Hello Worldgtlt/optiongt
    ltoption value=My Second Postgtlt/optiongt
    ltoption value=WordPress Tipsgtlt/optiongt
  lt/datalistgt
  ltbutton type=submitgtSearchlt/buttongt
lt/formgt

To output a datalist from PHP (in searchform.php or via a get_search_form filter), loop through get_posts() and echo safe options using esc_attr() / esc_html().

// Example: echoing datalist options in searchform.php (only for very small sites)
posts = get_posts( array(
  posts_per_page =gt 200,
  post_status =gt publish,
) )
if ( ! empty( posts ) ) {
  echo ltdatalist id=search-suggestionsgtn
  foreach ( posts as p ) {
    echo ltoption value= . esc_attr( get_the_title( p ) ) . gtn
  }
  echo lt/datalistgtn
}

Pros and cons

  • Pros: Simple, no JS/AJAX required beyond browser handling datalist behaviors.
  • Cons: Not scalable, adds page weight, cannot filter live on the server, no paging or relevance tuning.

Approach B — Recommended: Dynamic suggestions via the WordPress REST API

This approach provides a fast, scalable way to return suggestions as the user types. It uses a custom REST route that returns a small JSON payload (titles and optional permalinks). JavaScript fetches suggestions and updates the datalist. Use server-side caching to reduce DB load.

1) Add a REST route and callback (functions.php)

// Register REST route (put in themes functions.php or a small plugin)
add_action( rest_api_init, function () {
  register_rest_route( autocomplete/v1, /suggest, array(
    methods  =gt GET,
    callback =gt my_autocomplete_suggestions,
    args     =gt array(
      q =gt array(
        required =gt true,
        sanitize_callback =gt sanitize_text_field,
      ),
    ),
    permission_callback =gt __return_true, // public endpoint
  ) )
} )

function my_autocomplete_suggestions( WP_REST_Request request ) {
  q = request->get_param( q )
  if ( empty( q ) ) {
    return new WP_REST_Response( array(), 200 )
  }

  // Optional: use transient cache per query (short TTL)
  transient_key = ac_sugg_ . md5( q )
  cached = get_transient( transient_key )
  if ( cached !== false ) {
    return rest_ensure_response( cached )
  }

  // Query posts (tune post_type, post_status, fields)
  args = array(
    s                 =gt q,
    posts_per_page    =gt 10,
    post_status       =gt publish,
    fields            =gt ids,
    ignore_sticky_posts =gt true,
  )
  query = new WP_Query( args )

  results = array()
  if ( query-gthave_posts() ) {
    foreach ( query-gtposts as post_id ) {
      results[] = array(
        title =gt wp_strip_all_tags( get_the_title( post_id ) ),
        url   =gt get_permalink( post_id ),
      )
    }
  }

  // Cache results for a short time (30 seconds) to reduce DB load
  set_transient( transient_key, results, 30 )

  return rest_ensure_response( results )
}

Notes:

  • Use s query for basic matching. For more advanced matching (like postmeta or priority ranking), use custom queries or WP Search plugins.
  • Limit posts_per_page to reduce payload (e.g., 5–10 suggestions).
  • set_transient helps throttle repeated queries tune TTL to your update frequency.

2) Enqueue the JavaScript and pass REST root (functions.php)

// Enqueue script and pass REST info
add_action( wp_enqueue_scripts, ac_enqueue_autocomplete_script )
function ac_enqueue_autocomplete_script() {
  wp_register_script( ac-autocomplete, get_template_directory_uri() . /js/ac-autocomplete.js, array(), 1.0, true )
  wp_localize_script( ac-autocomplete, acData, array(
    root =gt esc_url_raw( rest_url() ),
    nonce =gt wp_create_nonce( wp_rest ), // optional for auth-protected routes
  ) )
  wp_enqueue_script( ac-autocomplete )
}

3) Markup for the search input datalist (searchform.php)

ltform role=search method=get id=searchform action=lt?php echo esc_url( home_url( / ) ) ?gtgt
  ltlabel for=s class=screen-reader-textgtSearch for:lt/labelgt
  ltinput type=search id=s name=s list=search-suggestions autocomplete=off placeholder=Search… aria-autocomplete=list /gt
  ltdatalist id=search-suggestionsgtlt/datalistgt
  ltbutton type=submitgtSearchlt/buttongt
lt/formgt

4) The JavaScript (ac-autocomplete.js)

(function () {
  // Configuration
  var MIN_CHARS = 2
  var DEBOUNCE_MS = 250
  var MAX_SUGGESTIONS = 10

  // Utility: debounce
  function debounce(func, wait) {
    var timeout
    return function () {
      var context = this, args = arguments
      clearTimeout(timeout)
      timeout = setTimeout(function () {
        func.apply(context, args)
      }, wait)
    }
  }

  // Initialize once DOM is ready
  document.addEventListener(DOMContentLoaded, function () {
    var input = document.querySelector(input[name=s], input#s)
    if (!input) return

    var listId = input.getAttribute(list)
    var datalist = listId ? document.getElementById(listId) : null

    // If browser doesnt support datalist, we still use JS to provide suggestions in console
    if (!datalist) {
      // optional: implement a custom dropdown fallback
      console.warn(No datalist found consider adding a datalist element or a custom fallback UI.)
      return
    }

    var root = (typeof acData !== undefined  acData.root) ? acData.root : /
    var endpoint = root   autocomplete/v1/suggest

    var fetchSuggestions = debounce(function () {
      var q = input.value.trim()
      if (q.length < MIN_CHARS) {
        // clear datalist
        datalist.innerHTML = 
        return
      }

      var url = endpoint   ?q=   encodeURIComponent(q)

      fetch(url, {
        method: GET,
        credentials: same-origin,
        headers: {
          Accept: application/json
        }
      })
      .then(function (res) {
        if (!res.ok) throw new Error(Network response was not ok)
        return res.json()
      })
      .then(function (data) {
        // Clear existing options
        datalist.innerHTML = 
        if (!Array.isArray(data)  data.length === 0) return

        // Add up to MAX_SUGGESTIONS options
        data.slice(0, MAX_SUGGESTIONS).forEach(function (item) {
          var option = document.createElement(option)
          // Use title for display value should be plain text (not HTML)
          option.value = item.title  
          datalist.appendChild(option)
        })
      })
      .catch(function (err) {
        // fail silently or log to console in dev
        console.error(Autocomplete error:, err)
      })
    }, DEBOUNCE_MS)

    input.addEventListener(input, fetchSuggestions)
  })
})()

How it works

  • User types into the search input.
  • JavaScript debounces input and calls the REST endpoint with the query string.
  • The REST callback returns an array of titles (and optionally URLs).
  • JS populates options inside the datalist element the browser shows suggestions in the native UI.

Optional: Linking suggestions to search results or direct navigation

Datalist options only supply a value for the input. If you want clicking a suggestion to immediately navigate to the post (instead of submitting a search), implement a small enhancement: when the user selects a datalist option, detect the selection and look up its URL from the last fetched results then redirect. Below is a JS snippet to do that. Note: datalist selection detection is browser-dependent listening for input change events and matching value to cached suggestions is common.

// Small extension to map selected title to URL and redirect
// Place inside the main script (after fetching data), keep a cache of lastSuggestions
var lastSuggestions = [] // set when fetch returns

// After fetching data:
lastSuggestions = data // data is array of {title, url}

// On input change, when the value exactly matches a suggestion, navigate
input.addEventListener(change, function () {
  var v = input.value.trim()
  var found = lastSuggestions.find(function (s) { return s.title === v })
  if (found  found.url) {
    window.location.href = found.url
  }
})

Alternative: Using admin-ajax.php (admin-ajax for older sites)

If you prefer the older admin-ajax method, register AJAX hooks and return JSON. This uses action hooks wp_ajax_nopriv_my_autocomplete and wp_ajax_my_autocomplete. Note: REST API is preferred for performance and caching.

// In functions.php
add_action( wp_ajax_nopriv_my_autocomplete, my_ajax_autocomplete )
add_action( wp_ajax_my_autocomplete, my_ajax_autocomplete )

function my_ajax_autocomplete() {
  q = isset( _GET[q] ) ? sanitize_text_field( wp_unslash( _GET[q] ) ) : 
  if ( empty( q ) ) {
    wp_send_json( array() )
  }

  posts = get_posts( array(
    s => q,
    posts_per_page => 10,
    post_status => publish,
    fields => ids,
  ) )

  results = array()
  foreach ( posts as p ) {
    results[] = array(
      title => wp_strip_all_tags( get_the_title( p ) ),
      url => get_permalink( p ),
    )
  }

  wp_send_json( results )
}
// JS fetch would call: /wp-admin/admin-ajax.php?action=my_autocompleteq=   encodeURIComponent(q)

Styling, accessibility and UX tips

  • Styling: The visual style of datalist dropdowns is set by the browser and cannot be easily customized. If you need fully styled suggestion lists, implement a custom dropdown (ul/li) and keyboard handling.
  • Accessibility: Include label and aria-autocomplete=list on the input. Use proper focus management if building a custom dropdown. Ensure suggestions are plain text (avoid HTML in option values).
  • Minimum chars: Set a minimum (2–3) to avoid many trivial requests.
  • Debounce: Debouncing (150–300ms) reduces request volume and improves perceived performance.
  • Throttle/caching: Use a server-side transient cache per query and consider client-side caching for repeated terms.
  • Escaping: Always escape titles when output in HTML and JSON (use wp_strip_all_tags / esc_html when appropriate) to avoid XSS.

Security, performance and best practices

  1. Limit results: Use a small posts_per_page (5–10) so the JSON payload remains tiny.
  2. Cache results: Use set_transient/get_transient to cache common queries for a short time (10–60 seconds), reducing DB load.
  3. Sanitize input: sanitize_text_field on incoming queries. Use prepared WP_Query parameters (avoid raw SQL).
  4. Escape output: Escape titles in HTML and JSON in the REST callback use wp_strip_all_tags and then return plain strings.
  5. Rate limiting: If your site gets heavy traffic or bots, consider server-level throttling or a small rate-limiting layer.
  6. Use HTTPS: Ensure requests use HTTPS to protect user privacy.
  7. Performance: If search becomes complex, consider specialized search solutions (ElasticPress, Algolia, SearchWP).

Limitations of datalist

  • Browser-dependent styling and behavior some older browsers may not support datalist well.
  • No built-in keyboard selection APIs beyond native behavior custom dropdowns offer more control.
  • Option elements only support a value attribute — to map to URLs you must manage a client-side lookup cache.

Browser support note

Most modern browsers support datalist. For older browsers or for advanced UX (rich items with images or meta), implement a custom dropdown and ARIA roles (listbox/option).

Example CSS for input (optional)

/ Simple styling for the search input datalist popup styling is browser-controlled /
input[type=search]#s {
  padding: 8px 10px
  border: 1px solid #ccc
  border-radius: 4px
  width: 100%
  box-sizing: border-box
  font-size: 16px
}

Debugging tips

  • Open DevTools Network tab and inspect the request to the REST endpoint check status code and returned JSON.
  • Temporarily log JSON in JS (console.log) to inspect structure.
  • Ensure the datalist element id matches the input list attribute exactly.
  • Check that your REST route returns results when you hit it directly in the browser (example: https://your-site.com/wp-json/autocomplete/v1/suggest?q=test).

Summary / Checklist to implement

  1. Add the search input and an empty datalist to your search form (searchform.php).
  2. Register a REST route that returns an array of suggestion objects (title optional url) and add server-side caching.
  3. Enqueue a JS file that fetches suggestions, debounces input, and fills the datalist with ltoptiongt elements.
  4. Sanitize and escape everywhere limit and cache results for performance.
  5. Test on desktop and mobile browsers and add a custom fallback if datalist support is required on legacy browsers.

For further reading see the WordPress REST API docs and consider search-specific plugins if you need fuzzy search, weighting, or very large datasets.

Complete minimal package (quick summary of files)

  • functions.php: register_rest_route enqueue script
  • searchform.php: input datalist
  • js/ac-autocomplete.js: the JavaScript shown above

With the code and configuration above you will have a functional, secure, and performant autocomplete experience using datalist and JavaScript in WordPress.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

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