datastar

package module
v1.27.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package datastar provides templ components and helpers for integrating Datastar — a ~12 KiB zero-dependency frontend framework that unifies HTMX-style backend reactivity with Alpine.js-style frontend reactivity via Server-Sent Events (SSE) and reactive signals.

This package mirrors the htmx package: it emits data-* attributes and injects the Datastar runtime <script> tag without importing any server-side SDK. The pinned version (DatastarVersion1_0_3) is derived from github.com/larsartmann/go-datastar/static.Version so the CDN URL and the embedded bundle can never drift.

Server-side SDK

Consumers who want SSE streaming should use github.com/larsartmann/go-datastar — a protocol library where every patch is a first-class value (not a method call on a live connection). Add it to your own go.mod:

go get github.com/larsartmann/go-datastar

Self-hosting the Datastar runtime

By default, SDKScript loads the runtime from the jsDelivr CDN. For self-hosting, use github.com/larsartmann/go-datastar/static (zero dependencies — its go.mod requires nothing):

mux.Handle("GET /datastar.js", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
    w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
    _, _ = w.Write(static.Bytes())
}))
@datastar.SDKScript(datastar.SDKScriptProps{Src: "/datastar.js"})

Quick start

Inject the runtime once per page (in your layout, alongside or instead of the HTMX script):

@datastar.SDKScript(datastar.DefaultSDKScriptProps())

Establish an SSE-powered live region:

@datastar.LiveRegion(datastar.LiveRegionProps{
    URL:       "/stream/metrics",
    AutoStart: true,
}) {
    @display.StatCard(display.StatCardProps{
        BaseProps: utils.BaseProps{ID: "metrics"},
        Label:     "Active Users",
        Value:     "—",
    })
}

Two options are load-bearing on real deployments:

  • Retry: RetryAlways — the runtime default ('auto') never reconnects after a clean stream EOF, which is exactly what a server restart produces. Without it the region goes permanently stale.
  • Cancellation: CancellationCleanup — when an HTMX swap replaces the region (e.g. a filter change re-renders it with a different URL), cleanup aborts the old stream so it cannot keep re-patching the region with stale content. The swapped-region combination renders @get(url, {retry: 'always', requestCancellation: 'cleanup'}).

The server endpoint streams patches using go-datastar. Target a child element by selector — patch modes other than the default outer mode require one, and the default outer mode matches incoming root elements by their id (id-less fragments are dropped with a console warning):

func streamHandler(w http.ResponseWriter, r *http.Request) {
    stream := sse.NewStream(w, r)
    defer func() { _ = stream.Close() }()
    resp := datastar.NewResponse(stream)
    _ = resp.PatchElementsTempl(metricsCardContent(),
        datastar.WithSelector("#metrics"), datastar.WithModeInner())
}

Patched fragments carry no executable scripts

The runtime inserts patched elements with innerHTML/morph semantics, and script elements inserted via innerHTML are inert — they are NEVER executed. This differs from htmx, which re-evaluates scripts in swapped content. A server patch therefore cannot ship behavior as an inline <script> tag: drive client-side reactions with data-* attributes and expressions on the patched elements instead (bundle-proven fact — see docs/datastar-runtime-facts.md, "Fragments carry no executable scripts").

When to choose Datastar over HTMX

See docs/research/datastar-integration-analysis.md for the full analysis. Briefly: HTMX is the default (zero JS dependency). Datastar is opt-in for real-time streaming, reactive client-side state, or when the consumer's app already uses Datastar.

Re-audit contract (for contributors)

The runtime bundle this package integrates against is an EXTERNAL dependency pinned in go.mod (go-datastar/static). Its wire behavior is documented in docs/datastar-runtime-facts.md (with a bundle-provenance block: pin version, byte size, sha256, extraction commands) and enforced by tests:

  • datastar.TestPinnedRuntimeBundleContract pins the bundle sha256 plus every integration token (event names, retry literals) — a pin bump that changes any pinned byte fails there first.
  • examples/demo/sse_test.go pins the SSE wire format the demo streams.

When bumping the pin, follow docs/external-dependency-bumps.md end to end: verify-at-source, diff old-vs-new bundle, bump, re-audit every fact in docs/datastar-runtime-facts.md, update pinnedBundleSHA256 in bundle_guard_test.go, and warm [Unreleased] in CHANGELOG.md.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func DatastarVersionIsValid

func DatastarVersionIsValid(v DatastarVersion) bool

DatastarVersionIsValid reports whether v is one of the defined DatastarVersion constants.

func Delete

func Delete(url string) string

Delete returns a Datastar @delete('url') action expression.

func Get

func Get(url string) string

Get returns a Datastar @get('url') action expression for use in data-on:* or data-init attributes. The response should be an SSE stream.

data-on:click={ datastar.Get("/api/search") }

func Indicator

func Indicator(props IndicatorProps) templ.Component

Indicator renders a loading indicator that shows/hides based on a Datastar indicator signal. Place data-indicator:<name> on the element that triggers a backend action, then render an Indicator wherever you want the spinner.

This is the Datastar equivalent of htmx.LoadingIndicator.

<button data-on:click="@post('/save')" data-indicator:saving>Save</button>
@datastar.Indicator(datastar.IndicatorProps{Signal: "saving"})

With a custom spinner:

@datastar.Indicator(datastar.IndicatorProps{
    Signal:  "saving",
    Spinner: feedback.Spinner(feedback.SpinnerProps{Size: feedback.SpinnerSM}),
})
Example
package main

import (
	"github.com/larsartmann/templ-components/datastar"
)

func main() {
	_ = datastar.Indicator(datastar.DefaultIndicatorProps())
}

func LivePolitenessIsValid

func LivePolitenessIsValid(v LivePoliteness) bool

LivePolitenessIsValid reports whether v is one of the defined LivePoliteness constants.

func LiveRegion

func LiveRegion(props LiveRegionProps) templ.Component

LiveRegion renders a container that auto-connects to an SSE endpoint and receives server-patched updates. This is the Datastar equivalent of htmx.PolledRegion — but instead of polling on an interval, the server pushes patches over a single long-lived connection.

An empty URL degrades to a plain container (no data-init) — the runtime throws FetchNoUrlProvided for @get(”). Wire the stream via data-on:* on a trigger element instead.// The server endpoint must return a text/event-stream response. Use go-datastar (github.com/larsartmann/go-datastar) to patch elements or signals. Target a child element of the region with WithSelector — patch modes other than the default (outer) require one, and the default outer mode matches incoming root elements by their id (id-less fragments are dropped with a console warning):

func streamHandler(w http.ResponseWriter, r *http.Request) {
    stream := sse.NewStream(w, r)
    defer func() { _ = stream.Close() }()
    resp := datastar.NewResponse(stream)
    _ = resp.PatchElementsTempl(metricsCardContent(),
        datastar.WithSelector("#metrics"), datastar.WithModeInner())
}

@datastar.LiveRegion(datastar.LiveRegionProps{
    URL:       "/stream/metrics",
    AutoStart: true,
}) {
    @display.StatCard(display.StatCardProps{
        BaseProps: utils.BaseProps{ID: "metrics"},
        Label:     "Active Users",
        Value:     "—",
    })
}
Example
package main

import (
	"github.com/larsartmann/templ-components/datastar"
)

func main() {
	_ = datastar.LiveRegion(datastar.DefaultLiveRegionProps())
}

func LoadingButton added in v1.20.0

func LoadingButton(props LoadingButtonProps) templ.Component

LoadingButton renders a button label that swaps to a busy state while the enclosing button's Datastar action is in flight — the Datastar equivalent of htmx.LoadingButton, driven entirely by the runtime's indicator signal (zero component-side JavaScript).

The component renders CONTENT, not the button: place it inside a button that carries BOTH the action and the indicator marker, and pass the same signal name here. While the action is in flight the runtime sets the signal true (bundle-verified), data-show swaps the labels, and the signal clears when the response lands.

Unlike the htmx twin there is no disabled-element mechanism in the Datastar dialect; gate double submissions server-side or via data-indicator-driven UI.

<button
    data-indicator:saving
    data-on:click="@post('/save', {contentType: 'form'})"
    class="inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-white"
>
    @datastar.LoadingButton(datastar.LoadingButtonProps{
        Signal:      "saving",
        DefaultText: "Save",
        LoadingText: "Saving…",
    })
</button>
Example
package main

import (
	"github.com/larsartmann/templ-components/datastar"
)

func main() {
	_ = datastar.LoadingButton(datastar.LoadingButtonProps{
		Signal:      "saving",
		DefaultText: "Save",
		LoadingText: "Saving…",
	})
}

func Patch

func Patch(url string) string

Patch returns a Datastar @patch('url') action expression.

func PolledRegion added in v1.20.0

func PolledRegion(props PolledRegionProps) templ.Component

PolledRegion renders a div that auto-refreshes its content via Datastar interval polling (data-on-interval). This is the Datastar twin of htmx.PolledRegion: the region fetches its own URL on an interval and the endpoint responds with a fragment whose root id matches the region id, so the runtime's default outer-mode patch replaces the region (self-patch). The runtime re-initializes the patched-in element, so polling continues with a fresh interval — and because this component never emits the leading flag, a self-patch CANNOT loop eagerly (leading + outer self-patch would refetch immediately forever; that combination stays out of the API on purpose — the same hazard class as htmx's forbidden "load, every" trigger on a self-replacing region).

Initial content belongs in the children slot (server-rendered), so the region never shows an empty state and needs no eager-fetch mechanism.

Polling busy state: the runtime wraps every tick in its fetching markers — attach data-indicator:<name> consumers via datastar.Indicator for spinners (see the Indicator docs). This component intentionally does not render aria-busy: there is no "waiting for first load" window to announce.

@datastar.PolledRegion(datastar.PolledRegionProps{
    BaseProps: utils.BaseProps{ID: "stats"},
    URL:       "/partials/stats",
    Every:     "10s",
}) {
    @display.StatCard(display.StatCardProps{Label: "Messages", Value: "1,234"})
}
Example
package main

import (
	"github.com/larsartmann/templ-components/datastar"
)

func main() {
	_ = datastar.PolledRegion(datastar.PolledRegionProps{
		ID:    "stats",
		URL:   "/partials/stats",
		Every: "10s",
	})
}

func Post

func Post(url string) string

Post returns a Datastar @post('url') action expression. Sends all signals as the request body.

func Put

func Put(url string) string

Put returns a Datastar @put('url') action expression.

func RetryModeIsValid added in v1.9.0

func RetryModeIsValid(v RetryMode) bool

RetryModeIsValid reports whether v is one of the defined RetryMode constants.

func SDKScript

func SDKScript(props SDKScriptProps) templ.Component

SDKScript injects the Datastar runtime as an ES module. Include this once per page in your layout, alongside or instead of the HTMX script.

Datastar loads as type="module" (deferred by default — no render-blocking). When loading from a CDN, a <link rel="preconnect"> to the CDN origin is emitted first so the module fetch skips DNS + TLS on the critical path.

@datastar.SDKScript(datastar.DefaultSDKScriptProps())

Self-hosting:

@datastar.SDKScript(datastar.SDKScriptProps{Src: "/static/datastar.js", Nonce: nonce})
Example
package main

import (
	"github.com/larsartmann/templ-components/datastar"
)

func main() {
	_ = datastar.SDKScript(datastar.DefaultSDKScriptProps())
}

func SSEErrorHandling

func SSEErrorHandling(cfg SSEErrorHandlingConfig) templ.Component

SSEErrorHandling renders a global listener for DataStar stream failures and surfaces them as accessible toasts so streaming errors are never silent. This is the DataStar counterpart to htmx.GlobalErrorHandling, which covers HTMX request errors.

The DataStar runtime (v1.x) reports the fetch/SSE lifecycle on the document-level "datastar-fetch" event. detail.type is one of "started", "finished", "error", "retrying", or "retries-failed" (plus the applied patch event names for successful SSE messages). This handler reports the two terminal failure states:

  • "error": the endpoint answered with an HTTP error (status >= 400). Toasted with the status code from detail.argsRaw.status.
  • "retries-failed": the connection dropped and automatic reconnection exhausted its retries (default: 10 attempts, exponential backoff). Toasted; the stream stays dead until its action re-runs.

Transient "retrying" events are logged to the console only — the runtime keeps reconnecting on its own, and toasting each attempt would spam.

Requires feedback.ToastContainer on the page for toast feedback (it provides the global tcShowToast function). Without it, errors are still logged to the console and announced via the aria-live region.

Typical layout wiring:

@feedback.ToastContainer("")
@datastar.SSEErrorHandling(datastar.DefaultSSEErrorHandlingConfig())
Example
package main

import (
	"github.com/larsartmann/templ-components/datastar"
)

func main() {
	_ = datastar.SSEErrorHandling(datastar.DefaultSSEErrorHandlingConfig())
}

Types

type DatastarVersion

type DatastarVersion string

DatastarVersion is a pinned Datastar runtime version. Use the exported constants (e.g. DatastarVersion1_0_3) for compile-time safety; custom versions can be constructed via DatastarVersion("x.y.z") but will render without SRI (no integrity hash — Datastar does not publish official SRI hashes; self-host and compute your own if integrity is required).

const (
	// DatastarVersion1_0_3 is the Datastar runtime version this package pins.
	// The value is derived from [static.Version] so the embedded bundle and the
	// CDN URL can never drift apart.
	DatastarVersion1_0_3 DatastarVersion = DatastarVersion(static.Version)

	// DatastarVersion1_0_2 is a compile-compatibility alias for downstream
	// consumers still referencing the pre-1.0.3 constant name
	// (go-health-dashboard v0.5.0 broke at compile time under the v1.13.1
	// rename — MVS resolves their templ-components/datastar require to the
	// highest version). Unlike the pin constant it is a LITERAL "1.0.2": it
	// renders the 1.0.2 CDN runtime, deliberately NOT derived from
	// [static.Version], so the name keeps telling the truth. Deprecated:
	// consumers should move to DatastarVersion1_0_3 (or drop the explicit
	// Version to follow the pin).
	DatastarVersion1_0_2 DatastarVersion = "1.0.2"
)

type IndicatorProps

type IndicatorProps struct {
	utils.BaseProps

	// Signal is the indicator signal name (without the $ prefix).
	// Must match the data-indicator:<name> on the triggering element.
	Signal string

	// Spinner is the loading animation to display while the signal is true.
	// Typically feedback.Spinner(...). When nil, a CSS pulse is shown.
	Spinner templ.Component
}

IndicatorProps configures a loading indicator that shows/hides based on a Datastar indicator signal. Place data-indicator:<name> on the element that triggers a backend action, then render an Indicator wherever you want the spinner to appear.

This is the Datastar equivalent of htmx.LoadingIndicator / InlineLoadingOverlay.

<button data-on:click="@post('/save')" data-indicator:saving>Save</button>
@datastar.Indicator(datastar.IndicatorProps{Signal: "saving"})

func DefaultIndicatorProps

func DefaultIndicatorProps() IndicatorProps

DefaultIndicatorProps returns sensible defaults for an indicator.

type LivePoliteness

type LivePoliteness string

LivePoliteness is the aria-live politeness setting for a live region. Controls how screen readers announce streamed content updates.

const (
	// LivePolite announces updates after the current speech finishes (default).
	LivePolite LivePoliteness = "polite"
	// LiveAssertive announces updates immediately, interrupting current speech.
	// Use sparingly — only for critical alerts.
	LiveAssertive LivePoliteness = "assertive"
	// LiveOff disables screen-reader announcements for this region.
	LiveOff LivePoliteness = "off"
)

type LiveRegionProps

type LiveRegionProps struct {
	utils.BaseProps

	// URL is the SSE endpoint that streams element/signal patches.
	// The server opens a long-lived connection and pushes updates.
	URL string

	// AutoStart opens the SSE connection on page load via data-init.
	// When false, the consumer triggers the stream manually (e.g. via a
	// button with data-on:click="@get('/stream')").
	// An empty or whitespace-only URL degrades to a plain container (no
	// data-init) — the runtime throws FetchNoUrlProvided for @get(”).
	AutoStart bool

	// Retry controls automatic reconnection when the stream ends or the
	// request fails. Defaults to RetryAuto: the runtime does NOT reconnect
	// after a clean stream EOF (e.g. a server restart) unless this is
	// RetryAlways. See RetryMode for the full behaviour matrix.
	Retry RetryMode

	// Cancellation controls whether the in-flight stream request is
	// aborted when this element is removed from the DOM. Set
	// CancellationCleanup when an HTMX swap replaces this region (e.g. a
	// filter change re-renders it with a different URL): without it the
	// old stream survives the swap and keeps re-patching the region with
	// stale content that races the replacement's stream. Defaults to
	// CancellationNone (the runtime default).
	Cancellation RequestCancellation

	// Live sets the aria-live politeness for screen-reader announcements.
	// Defaults to LivePolite.
	Live LivePoliteness
}

LiveRegionProps configures an SSE-powered auto-updating region. This is the Datastar equivalent of htmx.PolledRegion — but instead of polling on an interval, the server pushes patches via a long-lived SSE connection.

The URL endpoint should return a text/event-stream response. The server patches the region's children (or signals) using go-datastar (github.com/larsartmann/go-datastar). Patch a child by selector — see the example on LiveRegion in live_region.templ for the full handler.

@datastar.LiveRegion(datastar.LiveRegionProps{
    URL:       "/stream/metrics",
    AutoStart: true,
}) {
    @display.StatCard(display.StatCardProps{
        BaseProps: utils.BaseProps{ID: "metrics"},
        Label:     "Users",
        Value:     "—",
    })
}

func DefaultLiveRegionProps

func DefaultLiveRegionProps() LiveRegionProps

DefaultLiveRegionProps returns sensible defaults for a live region.

type LoadingButtonProps added in v1.20.0

type LoadingButtonProps struct {
	utils.BaseProps

	// Signal is the indicator signal name (without the $ prefix). Must
	// match the data-indicator:<name> attribute on the enclosing button.
	// An empty signal degrades gracefully: the default text always shows
	// and the busy state never triggers (mirrors Indicator's degradation).
	Signal string

	// DefaultText is the label shown at rest.
	DefaultText string

	// LoadingText is the label shown while the action is in flight.
	// An empty LoadingText keeps only the spinner in the busy state.
	LoadingText string

	// Spinner is the loading animation shown next to LoadingText. When nil,
	// a small inline CSS spinner is used (the same default as Indicator).
	Spinner templ.Component
}

LoadingButtonProps configures a button label that swaps to a busy state while the enclosing button's Datastar action is in flight. This is the Datastar equivalent of htmx.LoadingButton.

The component renders CONTENT, not the button itself: place it inside a button that carries both the action (data-on:click=...) and the indicator marker (data-indicator:<signal>), and pass the SAME signal name here.

func DefaultLoadingButtonProps added in v1.20.0

func DefaultLoadingButtonProps() LoadingButtonProps

DefaultLoadingButtonProps returns sensible defaults for a loading button.

type PolledRegionProps added in v1.20.0

type PolledRegionProps struct {
	utils.BaseProps

	// URL is the endpoint fetched on every tick. It should return the
	// refreshed region fragment (root element id = this region's id).
	// An empty URL renders NO interval attribute — the region stays a plain
	// static container (the runtime throws FetchNoUrlProvided for @get(”)).
	URL string

	// Every is the poll interval. Accepted shapes: "500ms", "10s", "2m",
	// "1h" (m/h are converted to seconds — the runtime's duration parser
	// would silently read them as MILLISECONDS, a 1ms self-poll storm from a
	// natural-looking typo), or a bare number (milliseconds, matching the
	// runtime). An empty Every defaults to "10s". Anything else passes
	// through verbatim (exotic timing stays the runtime's business).
	Every string

	// Live sets the aria-live politeness for screen-reader announcements.
	// Defaults to LivePolite.
	Live LivePoliteness

	// ShowTimestamp renders an "Updated HH:MM:SS" footer so operators can
	// see at a glance that polling is active (the timestamp ticks forward on
	// each successful poll because the whole region re-renders).
	ShowTimestamp bool

	// Now overrides the wall clock used for the timestamp footer. Nil (the
	// zero default) means time.Now. Inject a fixed clock in visual goldens
	// and tests so ShowTimestamp coverage is deterministic.
	Now func() time.Time

	// TimeFormat is the Go time format string for the timestamp footer.
	// Default: "15:04:05" (time-only). Use time.RFC3339 for full date+time.
	TimeFormat string
}

PolledRegionProps configures an auto-refreshing Datastar region. The region fetches its own URL on an interval (data-on-interval) and the endpoint responds with a fragment whose root id matches the region id, so the runtime's default outer-mode patch replaces the region — the same self-replacing model as htmx.PolledRegion.

func DefaultPolledRegionProps added in v1.20.0

func DefaultPolledRegionProps() PolledRegionProps

DefaultPolledRegionProps returns sensible defaults for a polled region.

type RequestCancellation added in v1.11.0

type RequestCancellation string

RequestCancellation controls what happens to an in-flight @get stream request when the element carrying data-init is removed from the DOM — typically an HTMX swap that replaces the region with a new instance pointing at a different URL.

const (
	// CancellationNone is the default: the option is omitted from the
	// action expression and the in-flight request survives element
	// removal. This is the runtime's own default behaviour.
	CancellationNone RequestCancellation = ""
	// CancellationCleanup aborts the request when its initiating element
	// is removed from the DOM. Without it, a swapped-out region's old
	// stream keeps running and re-patches the region with stale content
	// that races the replacement's stream.
	CancellationCleanup RequestCancellation = "cleanup"
)

type RetryMode added in v1.9.0

type RetryMode string

RetryMode controls automatic reconnection of Datastar backend actions when a stream ends or a request fails. Mirrors the retry argument the runtime accepts on @get/@post/... action expressions.

Runtime behaviour matrix (behaviour verified against datastar.js v1.0.2; pinned tokens re-verified present in the v1.0.3 bundle on 2026-09-05):

  • clean stream EOF (e.g. server restart): only RetryAlways reconnects. The default auto disposes the connection, leaving the region stale.
  • HTTP status >= 400: RetryAlways and RetryError reconnect.
  • thrown network errors: every mode reconnects.
  • the failure counter resets on every successful (200) connect, so RetryAlways self-heals indefinitely across individual restarts.
const (
	// RetryAuto reconnects on network errors only (runtime default).
	RetryAuto RetryMode = "auto"
	// RetryAlways reconnects after clean stream EOF, HTTP errors, and
	// network errors. Use for long-lived streams that must self-heal
	// across server restarts.
	RetryAlways RetryMode = "always"
	// RetryError reconnects on HTTP (>=400) and network errors, but not
	// after a clean stream EOF.
	RetryError RetryMode = "error"
	// RetryNever never reconnects.
	RetryNever RetryMode = "never"
)

type SDKScriptProps

type SDKScriptProps struct {
	utils.BaseProps

	// Version pins the Datastar runtime version. Defaults to DatastarVersion1_0_3.
	// Set to "" to use the default.
	Version DatastarVersion

	// CDN overrides the CDN base URL. Defaults to the jsDelivr GitHub CDN.
	// Consumers can point this at their own origin or pnpm mirror.
	CDN string

	// Src overrides the full script URL — use for self-hosting.
	// When set, Version and CDN are ignored.
	Src string
}

SDKScriptProps configures injection of the Datastar runtime <script> tag. Include this once per page in your layout, alongside or instead of the HTMX script. Datastar loads as an ES module (deferred by default).

@datastar.SDKScript(datastar.DefaultSDKScriptProps())

func DefaultSDKScriptProps

func DefaultSDKScriptProps() SDKScriptProps

DefaultSDKScriptProps returns sensible defaults for the Datastar runtime script.

type SSEErrorHandlingConfig

type SSEErrorHandlingConfig struct {
	// Nonce is the CSP nonce for the inline script.
	Nonce string
	// DurationMS is the toast auto-dismiss delay in milliseconds (default: 6000).
	DurationMS int
}

SSEErrorHandlingConfig configures global DataStar SSE error handling.

func DefaultSSEErrorHandlingConfig

func DefaultSSEErrorHandlingConfig() SSEErrorHandlingConfig

DefaultSSEErrorHandlingConfig returns sensible defaults for SSE error handling.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL