Debounced live search
Type-to-filter with a 400ms debounce. The list refreshes as you type without spamming the server.
#When to use
Any searchable list, table, or autocomplete where the user types freely. A debounce of 300–500ms is the sweet spot: shorter feels twitchy on slow connections, longer feels sluggish.
#Markup
<input type="search" name="q"
hx-get="/products"
hx-target="#products_table"
hx-swap="outerHTML"
hx-trigger="input changed delay:400ms"
hx-push-url="true" />
<div id="products_table">
<partial name="_ProductsTable" model="Model" />
</div>
hx-trigger="input changed delay:400ms"fires after 400ms of typing silence and only when the value actually changed.hx-target="#products_table"swaps that element with the response.hx-swap="outerHTML"replaces the element itself, not just its children.hx-push-url="true"updates the browser URL so deep-links + back button work.
#Handler
public async Task<IActionResult> OnGetAsync(string? q)
{
var results = await _products.SearchAsync(q ?? "");
return this.PageOrPartial("_ProductsTable", new { Results = results });
}
One handler serves both the initial full page render and the fragment updates — PageOrPartial detects the HX-Request: true header.
#Gotchas
- Empty query: always accept
nullor""and return all results; don't 400 on missing input. - URL sync:
hx-push-url="true"makes the query string reflect the current search, so reload + deep-link work for free. - Result count in another region: render it inside
_ProductsTableso the swap covers both, or push it viahx-swap-oob="true"on a separate fragment in the response. - Loading state: htmx adds
htmx-requestclass to the triggering element while the request is in flight; style with.htmx-request { opacity: 0.5 }.
#Related
- Async autocomplete — same primitive, used as a combobox's data source.
- Paginated list — combine with pagination for long result sets.