Forms & htmx
Interactivity without a JavaScript framework. One attribute changes everything.
You will learn
- Handle HTML form submissions with POST
- The full-page-reload problem and why it feels broken
- Add htmx to submit forms without reloading
- The four htmx attributes you'll use 95% of the time
The problem
You build a form. The user types something. They click submit. The entire page reloads. The scroll position resets. The nav re-renders. For a simple "add item" action, reloading the whole page feels like demolishing a wall to replace a light switch.
This is how the web worked in 1999. You can do better — without writing JavaScript.
Step 1: A traditional form
Here's a form that submits via regular POST:
<form method="post" action="/api/items">
<input type="text" name="item" placeholder="New item">
<button type="submit">Add</button>
</form>
This works. The server receives the data, processes it, and returns a full HTML page. But the browser navigates to a new URL, the old page is gone, and the user sees a flash of white.
Step 2: Add one attribute
Now add hx-post:
<form hx-post="/api/items" hx-target="#list" hx-swap="afterbegin">
<input type="text" name="item" placeholder="New item">
<button type="submit">Add</button>
</form>
<div id="list">
<!-- items appear here -->
</div>
The form no longer reloads the page. htmx sends the POST in the background, receives the
server's HTML response, and inserts it as the first child of #list.
No JavaScript. No fetch(). No React.
<form method="post" action="/api/items">
<input name="item" placeholder="New item">
<button>Add</button>
</form>
<!-- Full page reload. Scroll resets.
User sees flash of white. -->The mental model
Traditional forms are like demolishing a wall to replace a light switch. htmx is like unscrewing just the switch plate. The server sends back a new switch plate (an HTML fragment), and htmx swaps it into the wall. Everything else stays untouched.
htmx doesn't replace your server rendering. It enhances it. The server still generates HTML — htmx just puts it in the right place without a full page navigation.
The four attributes
This is 95% of htmx:
hx-get/hx-post/hx-put/hx-delete— fire the requesthx-target— which element to update (CSS selector)hx-swap— how to insert the responseinnerHTML— replace the target's childrenouterHTML— replace the target itselfafterbegin— insert as first childbeforeend— insert as last childdelete— remove the target
hx-trigger— when to fire (clickdefault, alsoload,change,keyup delay:300ms)
That's it. Four attributes replace hundreds of lines of JavaScript.
Live demo: a counter button
The button below has zero custom JavaScript. It uses hx-post to send
a request to /api/learn/demo/incr. The server increments a counter stored in your session,
renders a new <button> element, and returns it. hx-swap="outerHTML"
replaces the old button with the new one.
Open DevTools → Network tab and watch each click: a POST goes out, an HTML fragment comes back, the button is replaced. No page reload. No JSON parsing. No client-side state management.
Progressive enhancement
htmx works on top of regular HTML. If JavaScript is disabled, a
<form hx-post="/foo"> falls back to a regular form POST. The server returns
the same HTML; htmx just makes it smoother. Your app degrades gracefully — that's a feature.
What does hx-swap="afterbegin" do?
When htmx isn't enough
htmx is request/response. The client asks, the server answers. For scenarios where the server needs to push updates without being asked — live notifications, multi-tab sync, AI token streaming — you need a persistent connection. ZealPHP has WebSocket (App::ws()) and Server-Sent Events ($response->sse()) for those cases. You'll use both in Lessons 9 and 10.
Key Takeaways
- htmx turns any HTML element into an AJAX trigger with just HTML attributes
- The server returns HTML fragments, not JSON — no client-side rendering needed
- Four attributes (
hx-post,hx-target,hx-swap,hx-trigger) cover 95% of use cases - Progressive enhancement: forms still work without JavaScript