hxgo provides request-header helpers and response-header builders for HTMX
v4. It works with Go's standard net/http package
and includes helpers for Echo, Fiber, and Gin.
This module, github.com/stackus/hxgo/v4, supports HTMX v4. Version 1
remains available for HTMX v2 users at github.com/stackus/hxgo, without the
/v4 module suffix.
The module requires Go 1.27.1 or later.
go get github.com/stackus/hxgo/v4import "github.com/stackus/hxgo/v4"Use request helpers to inspect HTMX request headers and Headers to add HTMX
response headers to a standard http.ResponseWriter.
package main
import (
"net/http"
"github.com/stackus/hxgo/v4"
)
func handler(w http.ResponseWriter, r *http.Request) {
if !hx.IsHtmx(r) {
return
}
err := hx.Headers(w,
hx.Location("/account").
Target("#content").
Swap(hx.SwapInnerHtml.IgnoreTitle()),
hx.Trigger(
"account-updated",
hx.Event("toast", map[string]string{"message": "Saved"}),
),
)
if err != nil {
http.Error(w, "unable to build HTMX response", http.StatusInternalServerError)
return
}
_, _ = w.Write([]byte("<p>Account updated.</p>"))
}The request helpers read HTMX request headers from *http.Request. Each
adapter package exposes the same helpers for its framework context.
| Request header | Constant | Helper |
|---|---|---|
HX-Boosted |
HxBoosted |
IsBoosted |
HX-Current-URL |
HxCurrentUrl |
GetCurrentUrl |
HX-History-Restore-Request |
HxHistoryRestoreRequest |
IsHistoryRestoreRequest |
HX-Request |
HxRequest |
IsRequest, IsHtmx |
HX-Request-Type |
HxRequestType |
GetRequestType, IsRequestTypeFull, IsRequestTypePartial |
HX-Source |
HxSource |
GetSource |
HX-Target |
HxTarget |
GetTarget |
Is* helpers report whether the relevant header is present, except
IsRequestTypeFull and IsRequestTypePartial, which compare
HX-Request-Type with full and partial. Get* helpers return an
empty string when their header is absent.
func handler(w http.ResponseWriter, r *http.Request) {
if hx.IsRequestTypePartial(r) {
// Render a partial response.
return
}
if hx.IsRequestTypeFull(r) {
// Render a full-page response.
}
}Headers applies HeaderOption values to an http.ResponseWriter. It
only sets response headers; write the status code and response body with the
usual facilities of your HTTP framework after calling it.
func handler(w http.ResponseWriter, r *http.Request) {
if err := hx.Headers(w,
hx.Retarget("#messages"),
hx.Reselect("#message-list"),
hx.PushUrl("/messages"),
); err != nil {
http.Error(w, "unable to build HTMX response", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("<ul id=\"message-list\"></ul>"))
}HeaderOption is the common option interface. HeaderOptionFunc is the
function form used by options such as Trigger, Refresh, ReplaceUrl,
and PushUrl.
BuildHeaders returns an *HxHeaders without writing a response. Use
HxHeaders.Headers() to obtain the constructed header map when integrating
with a package that does not use http.ResponseWriter.
headers, err := hx.BuildHeaders(
hx.Redirect("/sign-in"),
hx.Trigger("session-expired"),
)
if err != nil {
return err
}
for name, value := range headers.Headers() {
// Set name and value with the target framework's response API.
_ = name
_ = value
}| Response header | Constant | Option |
|---|---|---|
HX-Location |
HxLocation |
Location |
HX-Redirect |
HxRedirect |
Redirect |
HX-Refresh |
HxRefresh |
Refresh |
HX-Retarget |
HxRetarget |
Retarget |
HX-Reswap |
HxReswap |
Reswap or a Swap* constant |
HX-Reselect |
HxReselect |
Reselect |
HX-Replace-Url |
HxReplaceUrl |
ReplaceUrl |
HX-Push-Url |
HxPushUrl |
PushUrl |
HX-Trigger |
HxTrigger |
Trigger |
Redirect, Retarget, Reswap, and Reselect are string types that
can be passed directly to Headers. Refresh() sets HX-Refresh: true.
ReplaceUrl and PushUrl accept either a string URL or false to
disable the corresponding history behavior.
err := hx.Headers(w,
hx.Redirect("/sign-in"),
hx.Refresh(),
hx.ReplaceUrl(false),
hx.PushUrl("/projects"),
)Calling Location(path) sets HX-Location. With only a path, it writes
that path directly. Calling a fluent method makes the header a JSON object
containing the path and configured properties.
| Method | Location property |
|---|---|
Source(string) |
source |
Event(string) |
event |
Target(string) |
target |
Swap(string | Reswap | *ReswapModifiers) |
swap |
Transition() |
transition: "true" |
Values(any) |
values |
Headers(map[string]string) |
headers |
Select(string) |
select |
SelectOOB(string) |
selectOOB |
err := hx.Headers(w,
hx.Location("/search").
Source("button#search").
Event("click").
Target("#results").
Swap(hx.SwapInnerMorph.Transition()).
Select("#results").
SelectOOB("#notifications").
Values(map[string]string{"query": "go"}).
Headers(map[string]string{"X-Requested-With": "search"}),
)Reswap overrides the swap strategy selected by the triggering element.
Pass a custom Reswap string or one of these constants:
| Constant | Swap strategy |
|---|---|
SwapInnerHtml |
Replace an element's content. |
SwapOuterHtml |
Replace an entire element. |
SwapTextContent |
Replace text without parsing HTML. |
SwapBefore |
Insert before an element. |
SwapPrepend |
Insert as the first child. |
SwapAppend |
Insert as the last child. |
SwapAfter |
Insert after an element. |
SwapInnerMorph |
Morph an element's content. |
SwapOuterMorph |
Morph an entire element. |
SwapOuterSync |
Morph target attributes, then replace children. |
SwapDelete |
Remove the target. |
SwapNone |
Do not insert response content. |
SwapUpsert |
Update elements by ID and insert new elements. |
Modifiers are fluent and can be chained:
| Modifier | Behavior |
|---|---|
Transition() |
Enable view transitions. |
Swap(string | time.Duration) |
Set the swap delay. |
Settle(string | time.Duration) |
Set the settle delay. |
IgnoreTitle() |
Keep the current document title. |
ScrollTop() / ScrollBottom() |
Scroll to the target edge; pass true for the window or a selector for another target. |
ShowTop() / ShowBottom() |
Show the target edge; optionally pass another selector. |
ShowNone() |
Disable automatic show scrolling. |
FocusScroll(bool) |
Control scrolling when restored focus is brought into view. |
Target(string) |
Set the swap target. |
Strip(bool) |
Control removal of the response's outer element. |
SwapEmpty(bool) |
Control swaps for an empty response. |
err := hx.Headers(w,
hx.SwapOuterMorph.
Transition().
Swap("200ms").
Settle("100ms").
IgnoreTitle().
ScrollTop("#results").
ShowNone().
FocusScroll(true).
Target("#content").
Strip(false).
SwapEmpty(true),
)Trigger sets HX-Trigger. It accepts simple event names as strings,
TriggerEvent values returned by Event, or both. A simple event is
emitted as a comma-separated name. Event(name) creates a named event; one
data value is encoded directly, while two or more are encoded as an array.
err := hx.Headers(w,
hx.Trigger(
"refresh-sidebar",
hx.Event("toast", map[string]string{"message": "Saved"}),
hx.Event("selected", "first", "second"),
),
)The hxecho, hxfiber, and hxgin packages provide the same
request-header constants/helpers and a Headers function for their framework
context. They re-export response constants, option types, and convenience
functions needed to build headers with the adapter package.
| Framework | Import path | Response helper |
|---|---|---|
| Echo v5 | github.com/stackus/hxgo/v4/hxecho |
hxecho.Headers(*echo.Context, ...) |
| Fiber v3 | github.com/stackus/hxgo/v4/hxfiber |
hxfiber.Headers(fiber.Ctx, ...) |
| Gin | github.com/stackus/hxgo/v4/hxgin |
hxgin.Headers(*gin.Context, ...) |
For example, with Echo v5:
package main
import (
"net/http"
"github.com/labstack/echo/v5"
"github.com/stackus/hxgo/v4/hxecho"
)
func main() {
e := echo.New()
e.GET("/", func(c *echo.Context) error {
if hxecho.IsHtmx(c) {
if err := hxecho.Headers(c,
hxecho.Location("/account").Target("#content"),
hxecho.Trigger("account-loaded"),
); err != nil {
return err
}
}
return c.String(http.StatusOK, "Hello Echo")
})
e.Logger.Fatal(e.Start(":8080"))
}Contributions are welcome. Please open an issue or submit a pull request. Bug reports should include a clear description, reproduction steps, and useful logs or screenshots. Please check for existing issues before creating a new one.
This project is licensed under the MIT License. See LICENSE.