HTMX Examples: Copy-Paste Patterns for Forms, Search and Tabs

3 min read

Originally published in Turkish. Read the original →

HTMX Examples: Copy-Paste Patterns for Forms, Search and Tabs - cover image
Summary

Three HTMX patterns: submitting a form without a page reload, returning HTML fragments to a search box and loading tabs from the server. Each example uses a single feature.

Someone searching for HTMX examples isn't looking for the whole framework, they're looking for the form they'll use tomorrow. What HTMX is is covered in the What is HTMX post. The release notes are in the HTMX 4 post. The three patterns below fetch HTML fragments without a page reload.

Make sure the script is loaded on the page. The endpoints are sample addresses. Open the same path on your own server and return HTML.

Submitting a form in place

html
<form
  hx-post="/bulten"
  hx-target="#bulten-sonuc"
  hx-swap="innerHTML"
>
  <label for="eposta">Email</label>
  <input id="eposta" name="eposta" type="email" required />
  <button type="submit">Save</button>
</form>
<p id="bulten-sonuc"></p>

The /bulten response should not be a whole page but a single sentence or error line: Address saved.. This fragment is written into #bulten-sonuc. Return HTML on a validation error too. An empty body silently wipes the box.

Searching as you type

html
<input
  type="search"
  name="q"
  placeholder="Search"
  hx-get="/ara"
  hx-trigger="keyup changed delay:300ms"
  hx-target="#sonuclar"
  hx-swap="innerHTML"
/>
<div id="sonuclar"></div>

changed does not send a second request for the same text. delay:300ms fires not on every letter but after a short pause. /ara should return a list. If there are no results, return an empty-result sentence. That way the old list doesn't stay on screen.

Loading tab content

html
<div role="tablist">
  <button hx-get="/sekme/ozet" hx-target="#sekme" hx-swap="innerHTML">Summary</button>
  <button hx-get="/sekme/notlar" hx-target="#sekme" hx-swap="innerHTML">Notes</button>
</div>
<div id="sekme" role="tabpanel">Pick a tab.</div>

Each endpoint returns only the body of that tab. Don't send the menu again. Otherwise you end up with nested menus. The server has to provide the selected button's marker separately, outside the fragment it returns. This example has no such marker. On a small page the button text is enough.

Where to look when it breaks

If the request goes out and the screen doesn't change, the hx-target id and the element's id value don't match. If the response arrives and the page bloats, the endpoint is returning the whole document. Look at the response body in the network tab. If you see JSON, HTMX prints it like text. Switch the endpoint to HTML.

These three patterns are enough for a single-page panel. The server produces the calculations, permissions and error text. HTMX only puts that text inside the target.

Loading and errors

Don't leave the box empty while the request is running. You can add hx-disabled-elt="this" to the button and a short waiting text to the form. A more visible approach is a Saving sentence returned by the server inside the target. You don't need a separate endpoint for this. When the real response arrives, that sentence changes anyway.

Return HTML on an error code. The 400 body should be

Invalid address.

. An empty 500 body makes the user think the old result on screen is current. If HTMX doesn't write the error response to the target, show a separate line with hx-target-error. If there is no separate line, look at the network tab. If the request is red, the problem is in the endpoint, not the selector.

The address bar

If the address stays the same while the results in the search box change, the list is lost when the page is reloaded. hx-push-url="true" writes the search request's address to the bar. Use it only in the search pattern. The address should not change on a form save. A save is an action, not a page. Pushing the address ties the back button to the save and confuses the user.

For tabs, the address is optional too. In a small panel with three tabs, changing the address is not required. If a tab will be shared, hx-push-url goes on that button. Don't copy the decision to every button. If one pushes and another doesn't, the address and the screen drift apart.

Keep the fragment lean

The returned HTML should not contain html, head or another script. The browser embeds it into the target and the page binds behavior twice. The fragment should be a ul, a p or a table row. Don't put an element with an id inside the fragment using the same id as the target itself. Two identical ids cause the next request to write into the wrong box.

If your server template renders the whole page, pick a narrow template for HTMX requests. The request's HX-Request header is there for this. If the header is present, return the fragment; if not, the whole page. That way the same endpoint can be opened directly and also called by HTMX. You don't have to maintain two separate addresses.

Frequently Asked Questions (FAQ)

HTMX puts the HTML from the response inside the element pointed to by hx-target. The endpoint should return a small HTML fragment instead of JSON.
If you add delay to the hx-trigger value, the request goes out after typing stops. Without a delay, every letter becomes a separate request.
hx-post, hx-get, hx-target and hx-swap are the core attributes in these examples. For the release notes, see the HTMX 4 post on this site.