Skip to main content
Technology & EngineeringHtmx229 lines

Htmx Active Search

Active search, typeahead, and autocomplete patterns using HTMX triggers and debouncing

Quick Summary29 lines
You are an expert in building responsive search interfaces with HTMX, including live search, typeahead suggestions, and autocomplete components.

## Key Points

- **Debounce with `delay:300ms`.** 200-400ms is the sweet spot. Shorter delays increase server load; longer delays feel sluggish.
- **Always use the `changed` modifier.** This prevents requests when the user presses arrow keys or other non-character keys that do not change the input value.
- **Set `autocomplete="off"` on the input.** This prevents the browser's native autocomplete from conflicting with your HTMX-powered suggestions.
- **Use `hx-push-url` for search pages.** This lets users bookmark or share search results and preserves the back button.
- **Return an empty state message.** When no results match, return meaningful HTML like `<p class="no-results">No contacts found for "xyz".</p>` rather than an empty response.
- **Include a minimum query length.** On the server, skip the search and return an empty body (or a prompt message) if the query is shorter than 2-3 characters. This avoids overly broad queries.
- **Not debouncing.** Without `delay`, every keystroke triggers a server request. This hammers the server and causes UI flicker.
- **Leaking sensitive data in suggestions.** Autocomplete endpoints often return data before the user is specific. Ensure the endpoint respects authorization and does not expose private records.

## Quick Example

```html
<input type="text" name="city" value="New York" autocomplete="off">
<input type="hidden" name="city_id" value="1">
<ul id="suggestions" class="suggestion-list" role="listbox"></ul>
```

```html
<span id="result-count" hx-swap-oob="true">23</span>
<div class="product-card">...</div>
<div class="product-card">...</div>
<!-- ... more products ... -->
```
skilldb get htmx-skills/Htmx Active SearchFull skill: 229 lines
Paste into your CLAUDE.md or agent config

Active Search and Typeahead — HTMX

You are an expert in building responsive search interfaces with HTMX, including live search, typeahead suggestions, and autocomplete components.

Overview

Active search (also called "live search" or "instant search") updates results as the user types, without requiring a form submission. HTMX implements this with hx-trigger combined with keyup, input, or change events and a debounce delay. The server performs the search and returns rendered HTML results, keeping the implementation simple and the search logic centralized.

Core Concepts

Trigger with Delay

hx-trigger="input changed delay:300ms" fires 300ms after the user stops typing. The changed modifier ensures the request only fires when the value has actually changed. The delay modifier restarts the timer on each keystroke, acting as a debounce.

Request Indicators

While the search runs, hx-indicator shows a spinner. HTMX automatically cancels in-flight requests when a new one is issued from the same element, preventing out-of-order responses.

Request Deduplication

HTMX will not issue a new request if the parameters are identical to the previous request from that element. This prevents unnecessary server calls when the input has not meaningfully changed.

Implementation Patterns

Basic Live Search

<div class="search-container">
  <input type="search"
         name="q"
         placeholder="Search contacts..."
         hx-get="/contacts/search"
         hx-trigger="input changed delay:300ms, search"
         hx-target="#search-results"
         hx-indicator="#search-spinner"
         hx-swap="innerHTML"
         autocomplete="off">
  <span id="search-spinner" class="htmx-indicator">
    Searching...
  </span>
  <div id="search-results"></div>
</div>

The search event (triggered by the browser's clear button on type="search") ensures results update when the user clears the field.

Server response for /contacts/search?q=jane:

<div class="result-item">
  <a href="/contacts/12">Jane Doe</a>
  <span class="meta">jane@example.com</span>
</div>
<div class="result-item">
  <a href="/contacts/34">Janet Smith</a>
  <span class="meta">janet@company.com</span>
</div>

Typeahead / Autocomplete Dropdown

<div class="autocomplete" style="position: relative;">
  <input type="text"
         name="city"
         placeholder="Type a city..."
         hx-get="/cities/suggest"
         hx-trigger="input changed delay:200ms"
         hx-target="#suggestions"
         hx-swap="innerHTML"
         autocomplete="off"
         role="combobox"
         aria-expanded="false"
         aria-controls="suggestions">
  <ul id="suggestions"
      class="suggestion-list"
      role="listbox"
      style="position: absolute; top: 100%; left: 0; width: 100%;"></ul>
</div>

Server response for /cities/suggest?city=new:

<li class="suggestion" role="option"
    hx-get="/cities/select?id=1&name=New+York"
    hx-target="closest .autocomplete"
    hx-swap="innerHTML">
  New York
</li>
<li class="suggestion" role="option"
    hx-get="/cities/select?id=2&name=New+Orleans"
    hx-target="closest .autocomplete"
    hx-swap="innerHTML">
  New Orleans
</li>

When a suggestion is clicked, the server returns the input with the selected value:

<input type="text" name="city" value="New York" autocomplete="off">
<input type="hidden" name="city_id" value="1">
<ul id="suggestions" class="suggestion-list" role="listbox"></ul>

Search with Filtered Results Table

<div id="search-area">
  <input type="search" name="q"
         hx-get="/users"
         hx-trigger="input changed delay:300ms, search"
         hx-target="#results-body"
         hx-include="[name='role'], [name='status']"
         hx-indicator="#spinner"
         hx-push-url="true"
         placeholder="Search users...">

  <select name="role"
          hx-get="/users"
          hx-trigger="change"
          hx-target="#results-body"
          hx-include="[name='q'], [name='status']"
          hx-indicator="#spinner">
    <option value="">All Roles</option>
    <option value="admin">Admin</option>
    <option value="user">User</option>
  </select>

  <select name="status"
          hx-get="/users"
          hx-trigger="change"
          hx-target="#results-body"
          hx-include="[name='q'], [name='role']"
          hx-indicator="#spinner">
    <option value="">All Statuses</option>
    <option value="active">Active</option>
    <option value="inactive">Inactive</option>
  </select>

  <span id="spinner" class="htmx-indicator">Loading...</span>
</div>

<table>
  <thead>
    <tr><th>Name</th><th>Role</th><th>Status</th></tr>
  </thead>
  <tbody id="results-body">
    <!-- server-rendered rows -->
  </tbody>
</table>

Search with Result Count (Out-of-Band)

<p>Showing <span id="result-count">100</span> results</p>
<input type="search" name="q"
       hx-get="/products/search"
       hx-trigger="input changed delay:300ms"
       hx-target="#product-grid">
<div id="product-grid">
  <!-- products -->
</div>

Server response includes an out-of-band swap for the count:

<span id="result-count" hx-swap-oob="true">23</span>
<div class="product-card">...</div>
<div class="product-card">...</div>
<!-- ... more products ... -->

Highlighting Matched Text

The server handles highlighting by wrapping matches in <mark> tags:

<div class="result-item">
  <a href="/contacts/12"><mark>Jane</mark> Doe</a>
  <span class="meta"><mark>jane</mark>@example.com</span>
</div>

Core Philosophy

Active search with HTMX embraces the idea that the server is the best place to perform search logic. Rather than shipping a search index, fuzzy-matching algorithm, or filter engine to the client in JavaScript, you send keystrokes to the server and receive rendered HTML results. This keeps the search implementation centralized, consistent, and easy to evolve without redeploying client code.

The debounce-and-swap pattern is intentionally simple. HTMX's delay modifier acts as a built-in debounce, the changed modifier prevents redundant requests, and automatic request cancellation prevents stale results from overwriting fresh ones. These behaviors are declarative, visible in the HTML, and require no custom JavaScript. The complexity lives where it belongs: in the server's search query logic.

This approach also reinforces progressive enhancement. The search input can live inside a standard <form> with a submit button. Without JavaScript, the user types and submits. With HTMX, results appear live. The server handles both modes by checking the HX-Request header, returning either a full page or a fragment. The live search is a genuine enhancement, not a requirement.

Anti-Patterns

  • Firing a request on every keystroke without debouncing. Omitting delay in the trigger causes a server request for every character typed, hammering the server and causing UI flicker as responses arrive out of order.

  • Building client-side search indexing alongside HTMX search. Running a JavaScript search library (Lunr, Fuse.js) in the browser while also using HTMX for search creates two competing search systems with different results, confusing both developers and users.

  • Returning empty responses without feedback. When no results match, returning an empty response leaves users wondering if the search is broken. Always return a meaningful empty-state message like "No results found."

  • Searching on queries shorter than 2-3 characters. Single-character searches return overly broad results and waste server resources. Implement a minimum query length on the server and return a prompt message for short queries.

  • Using separate HTMX elements for different parts of the search result UI. Splitting the search input, result count, and result list across independent HTMX-triggered elements can cause out-of-order responses. Keep the search on a single element and use out-of-band swaps for secondary updates.

Best Practices

  • Debounce with delay:300ms. 200-400ms is the sweet spot. Shorter delays increase server load; longer delays feel sluggish.
  • Always use the changed modifier. This prevents requests when the user presses arrow keys or other non-character keys that do not change the input value.
  • Set autocomplete="off" on the input. This prevents the browser's native autocomplete from conflicting with your HTMX-powered suggestions.
  • Use hx-push-url for search pages. This lets users bookmark or share search results and preserves the back button.
  • Return an empty state message. When no results match, return meaningful HTML like <p class="no-results">No contacts found for "xyz".</p> rather than an empty response.
  • Include a minimum query length. On the server, skip the search and return an empty body (or a prompt message) if the query is shorter than 2-3 characters. This avoids overly broad queries.

Common Pitfalls

  • Not debouncing. Without delay, every keystroke triggers a server request. This hammers the server and causes UI flicker.
  • Race conditions with out-of-order responses. HTMX cancels previous in-flight requests from the same element automatically, but if you use separate elements for different parts of the UI, responses can arrive out of order. Keep search logic on a single element.
  • Forgetting keyboard accessibility. A typeahead dropdown must support arrow key navigation and Enter to select. This requires a small amount of JavaScript or the use of a library. Add ARIA roles (combobox, listbox, option) for screen readers.
  • Leaking sensitive data in suggestions. Autocomplete endpoints often return data before the user is specific. Ensure the endpoint respects authorization and does not expose private records.
  • Not clearing results when input is empty. Handle the empty query case on the server by returning the default content or an empty result set. Alternatively, listen for the search event (clear button) to reset the view.

Install this skill directly: skilldb add htmx-skills

Get CLI access →