htmx Tutorial · Common Patterns · Lesson 16 of 30
Active Search
Search as the user types, without sending a request for every keystroke, and keep the results accessible.
The pattern
A search box that updates the results while the user types. The important details:
hx-trigger="input changed delay:300ms": wait for a pause, and skip requests when the text didn't change.searchin the trigger list also fires when the user clears the box with the ✕ button oftype="search".hx-sync="this:replace": if a slow request is still running, cancel it, so old results never overwrite newer ones.- An
htmx-indicatorshows the search is running. aria-live="polite"on the results lets screen readers announce the update.
<input type="search" name="q" placeholder="Search languages..." aria-controls="rows"
hx-get="/languages" hx-trigger="input changed delay:300ms, search" hx-target="#rows"
hx-sync="this:replace" hx-indicator="#searching">
<span id="searching" class="htmx-indicator">Searching...</span>
<table><thead><tr><th>Language</th><th>Year</th></tr></thead>
<tbody id="rows" aria-live="polite"></tbody></table>
<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
const langs = [["Python", 1991], ["JavaScript", 1995], ["Java", 1995], ["PHP", 1995], ["Go", 2009],
["Rust", 2015], ["TypeScript", 2012], ["Kotlin", 2011], ["Swift", 2014], ["C#", 2000]];
server.get("/languages", (req) => {
const q = (req.query.q || "").toLowerCase();
const rows = langs.filter(([name]) => name.toLowerCase().includes(q));
return rows.length ? rows.map(([n, y]) => `<tr><td>${server.escape(n)}</td><td>${y}</td></tr>`).join("")
: `<tr><td colspan="2">No match for "${server.escape(q)}"</td></tr>`;
}, { delay: 400 });
</script>
On the server this is an ordinary search query that returns table rows. Use a database LIKE (or full-text) query
with bound parameters, never by pasting the user's text into SQL.
Test yourself
Why add hx-sync="this:replace" to a search input?
So a slow old request is cancelled and can't overwrite newer resultsTo replace the input after each searchTo make the search case-insensitiveTo cache the results
Show answer
Answer: So a slow old request is cancelled and can't overwrite newer results
Responses can arrive out of order; replace aborts the request that's still running.