HTMX Search as You Type

Search as you type — also called live search or instant search — shows results while the user is still typing. The page updates after each pause in typing without a form submit or page reload. HTMX makes this pattern straightforward using the keyup trigger combined with the delay modifier.

Why Delay Matters

  WITHOUT delay:
  User types: "l" "a" "p" "t" "o" "p"
  Requests:    6 requests fired — one per keystroke
  Server:      Flooded with partial queries

  WITH delay:400ms:
  User types: "l" "a" "p" "t" "o" "p"  (pauses)
  Requests:    1 request fired after the pause
  Server:      Receives only "laptop" — clean query

The delay resets every time the user types. The request fires only after the user stops typing for the specified duration. This keeps your server load low and your results relevant.

Basic Live Search

<input
  type="text"
  name="q"
  placeholder="Search products..."
  hx-get="/search"
  hx-trigger="keyup delay:400ms changed"
  hx-target="#search-results"
  hx-swap="innerHTML">

<div id="search-results">
  <p>Start typing to search.</p>
</div>

The changed modifier tells HTMX to skip the request if the value in the input did not actually change since the last request. This prevents re-fetching when the user presses a non-character key like Shift or Ctrl.

Full Flow Diagram

  User types "lap" into search box
        |
        | 400ms pass (user paused)
        v
  HTMX sends: GET /search?q=lap
        |
        v
  Server queries database for products containing "lap"
        |
        v
  Server returns:
    <ul>
      <li>Laptop Stand</li>
      <li>Laptop Bag</li>
      <li>Laptop Cooling Pad</li>
    </ul>
        |
        v
  HTMX places that HTML into #search-results
        |
        v
  User sees results appear — no page reload

  User keeps typing "lapto"
        |
        | Timer resets — no request yet
        v
  User types "laptop" and pauses
        |
        | 400ms pass
        v
  HTMX sends: GET /search?q=laptop
  (cycle repeats with narrower results)

Server Implementation

Flask (Python)

@app.route('/search')
def search():
    q = request.args.get('q', '').strip()
    if not q:
        return '<p>Start typing to search.</p>'

    results = Product.query.filter(
        Product.name.ilike(f'%{q}%')
    ).limit(10).all()

    if not results:
        return '<p>No products found.</p>'

    items = ''.join(f'<li>{p.name} — ${p.price}</li>' for p in results)
    return f'<ul>{items}</ul>'

Node.js (Express)

app.get('/search', (req, res) => {
    const q = (req.query.q || '').trim().toLowerCase();
    if (!q) return res.send('<p>Start typing to search.</p>');

    const results = products.filter(p =>
        p.name.toLowerCase().includes(q)
    ).slice(0, 10);

    if (!results.length) return res.send('<p>No products found.</p>');

    const html = results.map(p => `<li>${p.name} — $${p.price}</li>`).join('');
    res.send(`<ul>${html}</ul>`);
});

Highlighting the Search Term in Results

Bolding the matched portion of each result helps users see why each item appeared. The server wraps matched text in a <strong> tag before returning the HTML fragment:

import re

@app.route('/search')
def search():
    q = request.args.get('q', '').strip()
    results = Product.query.filter(Product.name.ilike(f'%{q}%')).limit(10).all()

    def highlight(text, term):
        pattern = re.compile(re.escape(term), re.IGNORECASE)
        return pattern.sub(lambda m: f'<strong>{m.group()}</strong>', text)

    items = ''.join(
        f'<li>{highlight(p.name, q)}</li>' for p in results
    )
    return f'<ul>{items}</ul>'

A search for "lap" returns:

<ul>
  <li><strong>Lap</strong>top Stand</li>
  <li><strong>Lap</strong>top Bag</li>
</ul>

Adding a Loading Indicator to Search

<input
  type="text"
  name="q"
  placeholder="Search..."
  hx-get="/search"
  hx-trigger="keyup delay:400ms changed"
  hx-target="#search-results"
  hx-indicator="#search-spinner">

<span id="search-spinner" class="htmx-indicator">Searching...</span>

<div id="search-results"></div>

Showing "No Results" vs Empty State

ScenarioServer ReturnsUser Sees
Empty input<p>Start typing to search.</p>Prompt message
Query with results<ul>...</ul>List of matching items
Query with no results<p>No results for "xyz".</p>Clear no-results message

Preventing XSS in Search Results

Never insert user-supplied search terms directly into HTML without escaping them. If a user searches for <script>alert(1)</script>, unescaped output would execute that script in every visitor's browser. Always escape user input on the server before embedding it in the response HTML. Every server framework has a built-in escaping function — use it.

Key Takeaway

Live search with HTMX uses the keyup delay:400ms changed trigger on a text input. The delay batches keystrokes into a single request after the user pauses. The server queries the database and returns an HTML list of results. HTMX drops the results into the target div. The user sees matching items appear and narrow down in real time — with no JavaScript to write and no page to reload.

Leave a Comment

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