Skip to content

Live search

Live search is a search input wired to htmx so the results region updates as the user types. The form still works without JavaScript — htmx attributes hang off a real <form action="/items" method="get">.

<form class="hc-search" action="/items" method="get" role="search">
<input
class="hc-input"
type="search"
name="q"
placeholder="Search"
data-hx-get="/items"
data-hx-trigger="input changed delay:300ms, search"
data-hx-target="#results"
data-hx-swap="innerHTML"
data-hx-sync="closest form:replace">
<button class="hc-button" type="submit">Search</button>
</form>
<div id="results" aria-live="polite"></div>
AttributeEffect
data-hx-get="/items"GET the search endpoint.
data-hx-trigger="input changed delay:300ms, search"Fire 300ms after the user stops typing, or immediately on the native search event.
data-hx-target="#results"Replace the results container.
data-hx-swap="innerHTML"Swap the inner HTML of the target.
data-hx-sync="closest form:replace"If a new request starts while one is in flight, cancel the old one.

The form itself stays valid:

  • <form action="/items" method="get"> works without JavaScript — the submit button posts the same q parameter to the same URL.
  • type="search" provides the native clear button and the search event that htmx listens for.
  • role="search" exposes the form as a search landmark to assistive tech.

If you import @hypermedia-components/core/macros, you can write the same form as a single custom element:

<hc-live-search
action="/items"
target="#results"
name="q"
placeholder="Search items"
label="Search items"
delay="200ms"
submit-label="Search">
</hc-live-search>
<div id="results" aria-live="polite"></div>

<hc-live-search> replaces its children with the expanded form on connectedCallback. The macro does not create the results container — it only emits the search form so you can place results wherever the page wants.

Attributes:

AttributeDefaultNotes
action(required)Search endpoint; becomes both <form action> and data-hx-get.
target(required)data-hx-target selector for the results container.
nameqQuery parameter name on the input.
placeholderSearchInput placeholder.
label(omitted)Visible <label> text. If omitted, the input gets an aria-label instead.
aria-labelSearchFallback aria-label when no visible label.
delay300mshtmx debounce delay (substituted into data-hx-trigger).
submit-labelSearchSubmit-button text.
swapinnerHTMLdata-hx-swap mode.
no-submit(boolean)Omit the submit button.

The macro is optional — for any customization beyond these attributes, copy the expanded HTML below and edit it directly.

The server should return only the inner HTML of #results — not the surrounding <div>.

<!-- /items?q=foo -->
<ul class="hc-list">
<li><a href="/items/1">Item one</a></li>
<li><a href="/items/2">Item two</a></li>
</ul>

For the empty state, return explicit empty-state markup so the user knows the search ran:

<!-- /items?q=zzz -->
<p class="hc-field__message">No results for "zzz".</p>

Return the same HTML when a non-htmx request comes in (full page load after submitting the form without JavaScript). Servers can detect htmx requests via the HX-Request: true header and decide whether to wrap the response in a full page layout or send the fragment only.

To show a spinner while the search runs, add an htmx indicator that points at the results region:

<div id="results" aria-live="polite">
<!-- During a request htmx adds the `.htmx-request` class to the
triggering element; the `.htmx-indicator` spinner below reacts to
it. Drive a busy state from that class if you want one. -->
</div>
<span class="hc-spinner htmx-indicator" id="search-spinner" aria-hidden="true"></span>

Add data-hx-indicator="#search-spinner" to the input.

  • role="search" on the <form> exposes the search landmark.
  • aria-live="polite" on #results announces the new result count (or empty state) without interrupting the user’s typing.
  • The native type="search" input is the right primitive — it brings the clear button, IME-friendly behavior, and the search event for Enter / form submission.
  • Avoid moving focus into the results list automatically. Keyboard users should stay in the input; if you provide keyboard navigation into results, document the keys clearly.
  • Without JavaScript, the form submits to action="/items" and the server renders a full results page.
  • With htmx, the same submit endpoint returns the results fragment.
  • With JavaScript but htmx not yet loaded (e.g. deferred loading), the form still submits normally on Enter.

The trick is that the htmx attributes never replace the form’s fundamentals. action and method stay intact; htmx augments.

  • Input — the control.
  • Field — pair an input with a label when you need one.
  • Request action — general htmx-with-spinner pattern.