Transport Wiring
Wire components once and switch between HTMX and Datastar with one field — the transport-agnostic wiring contract.
One Action, Two Dialects
HTMX and Datastar are dialects of the same operation: a method on a URL, triggered by a DOM event, patching a target region. The utils/wire package encodes that operation as one typed struct and renders it in whichever dialect you configure:
import "github.com/larsartmann/templ-components/utils/wire"
// htmx (the default — zero value resolves to htmx per ADR-0030)
wire.Action{URL: "/api/items", Target: "#items"}
// → hx-get="/api/items" hx-target="#items"
// datastar — one field switches the transport
wire.Action{Transport: wire.TransportDatastar, URL: "/api/items"}
// → data-on:click="@get('/api/items')"
Using It With Components
Every component accepts the rendered attributes through BaseProps.Attrs:
@display.Button(display.ButtonProps{
BaseProps: utils.BaseProps{Attrs: action.Attributes()},
Text: "Load more",
})
Components that opt in take the action directly — display.Button has a typed Wire field:
@display.Button(display.ButtonProps{
Text: "Load more",
Wire: &wire.Action{URL: "/api/items", Target: "#items"},
})
The extreme case is display.KanbanBoard: an entire drag-and-drop board wired through one Wire action, with optimistic moves (pending register + failure revert) working identically under both runtimes. See the full guide.
One Endpoint, Both Transports
The two runtimes mark their requests differently, and they pick the patch region differently:
| Caller | Request marker | Region chosen by |
|---|---|---|
| htmx | HX-Request |
hx-target (client-side) |
| Datastar | Datastar-Request |
response headers (Datastar-Selector, Datastar-Mode) |
wire.Handler wraps your fragment handler so one endpoint serves both — Datastar callers get the response-header targeting, everyone else passes through:
mux.Handle("/api/items", wire.Handler(wire.PatchTarget{
Selector: "#items",
Mode: wire.PatchModeInner,
}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
// render the fragment component...
})))
wire.IsDatastar(r) / wire.IsHTMX(r) are available for custom branching.
Scope Boundaries
The contract covers the dialects' common subset only (ADR-0036). Transport-specific machinery stays in its module: polling (htmx.PolledRegion), out-of-band swaps (htmx.SwapOOB), confirm dialogs (htmx.ConfirmDelete), loading indicators (htmx.InlineLoadingOverlay, datastar.Indicator), and SSE streams (datastar.LiveRegion).
One deliberate asymmetry: Action.Target renders only for htmx. The pinned Datastar v1.0.2 runtime accepts no target option on fetch actions — targeting is response-driven, which is exactly what wire.Handler does for you.
Web Components
The library ships no custom elements (ADR-0033): Shadow DOM breaks Tailwind theming. Light-DOM custom elements defined by consumers compose perfectly with everything here — both runtimes traverse light DOM unchanged. See the consumer recipe.