htmx: When to Use hx-boost vs. Targeted hx-get/hx-post for Partial Updates
A decision guide for htmx: when hx-boost's full-body progressive enhancement is enough, when targeted hx-get/hx-post with hx-target is worth the extra server work, and how to validate both.
16 Jan 2026, 13:48 UTC

Every htmx project hits the same fork early: do you boost standard links and forms so the whole site behaves like a single-page app, or do you annotate individual elements with hx-get/hx-post and update only the fragments that change? Both are supported, both are stable, and they solve different problems. Picking the wrong one costs you either lost page state (too much boosting) or duplicated server routes and broken back buttons (too much fragment surgery).
The decision and its constraints
The decision is: which requests swap the whole body, and which swap a targeted fragment? The constraints that matter:
- State preservation. A full-body swap resets scroll position, focus, open modals, and any third-party widget DOM. A fragment swap preserves everything outside the target.
- Progressive enhancement. Boosted links and forms still work as normal navigations when JavaScript is disabled. Fragment-only widgets usually have no meaningful no-JS fallback.
- History and deep-linking. Boosted navigations push history entries automatically. Fragment requests do not, unless you opt in with
hx-push-url. - Server complexity. Fragment endpoints must return partial HTML, which means your server needs to distinguish htmx requests from full-page requests.
Comparing the two approaches
| Concern | hx-boost | hx-get/hx-post + hx-target |
|---|---|---|
| What gets replaced | Entire <body> | Only the element matched by hx-target |
| No-JS fallback | Yes — normal link/form behavior | Usually none without extra work |
| History entry | Automatic | Requires hx-push-url or the HX-Push-Url response header |
| Scroll/focus/modal state | Lost on each navigation | Preserved outside the target |
| Server response | Full HTML document | Partial HTML fragment only |
| Best fit | Conventional multi-page navigation | Interactive widgets: inline edit, live search, infinite scroll |
Trade-offs in practice
hx-boost is the low-effort win. Add it to <body> and every anchor and form inside becomes an AJAX request that swaps the response body into the page, keeping URLs normal and bookmarkable. The cost is that every navigation is a full-body replacement: if your header contains a dropdown or a chat widget with its own DOM state, that state vanishes on every click. You also pay for re-rendering regions that did not change.
Targeted requests give you control at the price of more moving parts. You choose the swap strategy — innerHTML to replace a container's contents, outerHTML when the triggering element itself must be replaced (the common case for inline editing, where a row becomes a form), beforeend/afterend for appending list items, delete for removals, and none when you only want out-of-band updates. Modifiers like swap and settle timing let you coordinate with CSS transitions. The trade-off: your server must return fragments, and nothing appears in the address bar unless you ask for it.
Two features soften the trade-offs. Out-of-band swaps (hx-swap-oob) let one response update several regions — for example, submitting a comment can replace the comment list and also update a counter elsewhere on the page, avoiding multiple separate requests. And the HX-Request header lets one server route serve both audiences: return the full page for normal requests, the fragment when the header is present.
A pragmatic default
Boost the body for navigation, then carve out targeted requests for genuinely interactive widgets. A concrete sketch (htmx 1.x/2.x attribute syntax; confirm modifier names against your pinned version):
<body hx-boost="true"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<nav><a href="/dashboard">Dashboard</a></nav>
<!-- interactive widget: live search replacing only results -->
<input type="search" name="q"
hx-get="/search" hx-target="#results"
hx-trigger="keyup changed delay:300ms"
hx-push-url="true">
<div id="results"><!-- server returns rows only --></div>
</body>
Run this in any page template; no special permissions are needed since it is client-side markup. Server-side, gate the response on the request header (pseudocode for any framework):
if request.headers.get("HX-Request"):
return render("search_results.html", rows=rows) # fragment
return render("search_page.html", rows=rows) # full page
The CSRF header matters: htmx issues real POST/PUT/DELETE requests, and backends that require tokens for non-GET verbs will reject them unless the token travels with the request, as in the hx-headers attribute above.
Validating the setup
- No-JS fallback: disable JavaScript and click a boosted link. It should perform a normal full navigation to the same URL.
- Fragment discipline: in browser devtools' network tab, trigger the search. Confirm the request carries
HX-Request: trueand the response contains only the results markup — no<html>or<body>tags. Returning a full document to a fragment target nests those tags and produces subtle rendering bugs. - History: after a fragment update with
hx-push-url, press the browser back button, then reload the pushed URL directly. Both should show the expected state. - State check: open a modal or scroll the page, then trigger a fragment update. The modal and scroll position should survive; they would not under a boosted swap.
Limitations
Boosting is all-or-nothing within the boosted element, so mixed needs require either targeted overrides or restructuring. Fragment endpoints multiply your rendering paths, and every interactive widget needs its own history decision. Attribute defaults and swap modifiers differ between htmx 1.x and 2.x, so verify against the version your project pins rather than assuming cross-version behavior.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.