N

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

html
<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

csharp
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 null or "" 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 _ProductsTable so the swap covers both, or push it via hx-swap-oob="true" on a separate fragment in the response.
  • Loading state: htmx adds htmx-request class to the triggering element while the request is in flight; style with .htmx-request { opacity: 0.5 }.