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
| Scenario | Server Returns | User 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.
