Datastar

Stario’s stario.datastar module builds Datastar 1.0 data-* attributes, declarative @… action strings, signal payloads, and server-sent events. Attribute names, action syntax, signal JSON on the wire, and SSE framing match the upstream Datastar reference. Load a 1.0-compatible browser bundle in the page; ModuleScript() defaults to v1.0.2 on the CDN.

python
from stario.datastar import data, at, SSE, ModuleScript, read_signals, FileSignal

For architecture patterns (hypermedia, CQRS-shaped reads and commands, Relay fan-out), see The go-to architecture.

Overview

Datastar connects server handlers to reactive HTML in the browser. On the server you emit data-* attributes with data.*, wire user actions with at.* strings inside data.on(...), read client signal state with read_signals, and push updates over one SSE stream per response via SSE(w).

Signal names use Python snake_case on the wire. Nested client state belongs in nested JSON objects, not dotted top-level keys.

Create one SSE(w) per response and call patch_elements, patch_signals, navigate, and execute_script on that instance so every event shares the same Writer and Content-Type: text/event-stream headers.

Namespaces (data / at)

data and at are the stock singleton instances of DatastarAttributes and DatastarActions. Each data.* helper returns an Attrs fragment (pre-rendered opening-tag attribute bytes). Pass those fragments alongside attribute dicts, before child content.

Each at.* helper returns an action string such as @get('/items') for use inside attributes like data.on("click", at.get("/items")).

For a separate attribute prefix (custom Datastar bundle with aliased data-star-* names), construct another DatastarAttributes("data-star-") instance.

python
from stario.datastar import at, data
from stario.markup import html as h
 
h.Button(data.on("click", at.post("/cart")), "Add")
h.Input(data.bind("email"), {"type": "email"})

Fetch and action options use None to omit a key and let the client apply its default.

Low-level wire helpers (Case, js_object, string_literal) live in stario.datastar.format — import them when building action payloads, not from the package root:

python
from stario.datastar.format import Case, js_object, string_literal

js_object() treats string values as JavaScript expressions; wrap literal text with string_literal(). JSEvent is a type hint for event names on data.on; runtime accepts any str.

Runtime

ModuleScript() emits the type="module" <script> that loads Datastar (typically in <head>). Override src= when self-hosting.

read_signals(req) parses the JSON signals blob Datastar sends on each request. GET and DELETE read the datastar query parameter; other methods read the request body. The return value is a plain dict; treat it as untrusted input.

FileSignal is a TypedDict describing one file object nested under a signal key when a file input is bound with data.bind. Datastar encodes files as signal values, not multipart/form-data. Each value has name, base64 contents, and optional mime.

python
from stario.datastar import FileSignal, read_signals
 
payload = await read_signals(c.req)
avatar: FileSignal = payload["avatar"]
# validate name, decode contents, size limits, …

ModuleScript(src='https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.2/bundles/datastar.js')

Load the Datastar client as a module script.

python
from stario.datastar import ModuleScript
from stario.markup import html as h
 
h.Head(ModuleScript())

async read_signals(req)

Parse the JSON signals blob Datastar sends.

GET and DELETE use the datastar query parameter. Other methods use the request body.

This is convenience parsing only. Incoming signals are untrusted; validate types, sizes, and nested shapes before using them as application data. SSE signal patches use snake_case top-level keys; nested client state should be sent as nested JSON objects.

python
from stario.datastar import SSE, read_signals
 
@app.post("/action")
async def action(c, w):
    sig = await read_signals(c.req)
    SSE(w).patch_signals({"n": int(sig.get("n", 0)) + 1})

Attributes

Authoritative attribute names, modifiers, and client behavior live in the Datastar attributes reference. Singular helpers (attr, class_, computed, style, signal) target one key; plural helpers (attrs, classes, computeds, styles, signals) set several at once.

Methods marked Datastar Pro in docstrings emit the same data-* names as upstream Pro; the open-source browser bundle ignores them unless Pro is enabled.

DatastarAttributes.attr(key, expression)

Set one HTML attribute from a reactive expression.

python
data.attr("title", "$item.label")
# Attrs(' data-attr:title="$item.label"')

DatastarAttributes.attrs(mapping)

Set several HTML attributes from a mapping of expressions.

python
data.attrs({"open": "sidebarOpen"})
# Attrs(' data-attr="{'open':sidebarOpen}"')

DatastarAttributes.bind(signal_name, *, prop=None, event=None)

Two-way bind an element value to a signal.

python
data.bind("email")
# Attrs(' data-bind="email"')
 
data.bind("is_checked", prop="checked", event="change")
# Attrs(' data-bind:is-checked__case.snake__prop.checked__event.change="is_checked"')

DatastarAttributes.class_(name, expression)

Toggle one CSS class from a reactive expression.

python
data.class_("hidden", "!$expanded")
# Attrs(' data-class:hidden="!$expanded"')

DatastarAttributes.classes(mapping)

Toggle several CSS classes from a mapping of expressions.

python
data.classes({"loading": "$pending"})
# Attrs(' data-class="{'loading':$pending}"')

DatastarAttributes.computed(key, expression)

Create one computed signal from a reactive expression.

python
data.computed("full_name", "$first + $last")
# Attrs(' data-computed:full-name__case.snake="$first + $last"')

DatastarAttributes.computeds(mapping)

Create several computed signals from expressions.

python
data.computeds({"full_name": "$a", "initials": "$b"})
# Attrs(' data-computed:full-name__case.snake="$a" data-computed:initials__case.snake="$b"')

DatastarAttributes.effect(expression)

Run a side effect when the element initializes or updates.

python
data.effect("el.focus()")
# Attrs(' data-effect="el.focus()"')

DatastarAttributes.ignore(self_only=False)

Skip Datastar processing for this element or subtree.

python
data.ignore()
# Attrs(' data-ignore')
 
data.ignore(self_only=True)
# Attrs(' data-ignore__self')

DatastarAttributes.ignore_morph()

Prevent backend patches from morphing this subtree.

python
data.ignore_morph()
# Attrs(' data-ignore-morph')

DatastarAttributes.indicator(signal_name)

Track in-flight fetch state in a signal.

python
data.indicator("saving")
# Attrs(' data-indicator="saving"')

DatastarAttributes.init(expression, *, delay=None, view_transition=False)

Run an expression on element initialization.

python
data.init("setup()")
# Attrs(' data-init="setup()"')
 
data.init("setup()", delay="200ms", view_transition=True)
# Attrs(' data-init__delay.200ms__viewtransition="setup()"')

DatastarAttributes.json_signals(*, include=None, exclude=None, terse=False)

Render signals as JSON text.

python
data.json_signals()
# Attrs(' data-json-signals')
 
data.json_signals(include=["email", "password"], terse=True)
# Attrs(' data-json-signals__terse="{'include':'email|password'}"')

DatastarAttributes.on(event, expression, *, once=False, passive=False, capture=False, delay=None, debounce=None, throttle=None, view_transition=False, target=None, prevent=False, stop=False, case='kebab')

Listen for an event.

python
data.on("click", "@get('/cart/count')")
# Attrs(' data-on:click="@get('/cart/count')"')
 
data.on("click", "$open = false", target="outside")
# Attrs(' data-on:click__outside="$open = false"')

DatastarAttributes.on_intersect(expression, *, threshold=None, once=False, exit=False, delay=None, debounce=None, throttle=None)

React to viewport intersection.

python
data.on_intersect("load()", threshold=0.25, once=True)
# Attrs(' data-on-intersect__threshold.25__once="load()"')

DatastarAttributes.on_interval(expression, *, duration='1s', leading=False, view_transition=False)

Run an expression on an interval.

python
data.on_interval("tick()")
# Attrs(' data-on-interval="tick()"')
 
data.on_interval("tick()", duration="2s", leading=True)
# Attrs(' data-on-interval__duration.2s.leading="tick()"')

DatastarAttributes.on_signal_patch(expression, *, delay=None, debounce=None, throttle=None, include=None, exclude=None)

React to signal patches.

python
data.on_signal_patch("save()", debounce="500ms", include=["draft"])
# Attrs(' data-on-signal-patch__debounce.500ms="save()" data-on-signal-patch-filter="{'include':'draft'}"')

DatastarAttributes.preserve_attr(attrs)

Preserve selected attributes during DOM morphing.

python
data.preserve_attr(["data-testid", "id"])
# Attrs(' data-preserve-attr="data-testid id"')

DatastarAttributes.ref(signal_name)

Store the current element in a signal.

python
data.ref("search_input")
# Attrs(' data-ref="search_input"')

DatastarAttributes.show(expression)

Show or hide an element from a boolean expression.

python
data.show("$error != null")
# Attrs(' data-show="$error != null"')

DatastarAttributes.signal(name, expression, *, if_missing=False)

Patch one signal from a Datastar expression.

python
data.signal("my_count", "0", if_missing=True)
# Attrs(' data-signals:my-count__case.snake__ifmissing="0"')

DatastarAttributes.signals(payload, *, if_missing=False)

Patch several signals.

python
data.signals({"count": 0, "open": False})
# Attrs(" data-signals='{"count":0,"open":false}'")

DatastarAttributes.style(prop, expression)

Set one inline style property from a reactive expression.

python
data.style("width", "$pct + '%'")
# Attrs(' data-style:width="$pct + '%'"')

DatastarAttributes.styles(mapping)

Set several inline style properties from expressions.

python
data.styles({"opacity": "$visible ? '1' : '0'"})
# Attrs(' data-style="{'opacity':$visible ? '1' : '0'}"')

DatastarAttributes.text(expression)

Bind text content to a Datastar expression.

python
data.text("$greeting")
# Attrs(' data-text="$greeting"')

DatastarAttributes.animate(expression)

Animate element attributes over time. Datastar Pro only.

python
data.animate("$x")
# Attrs(' data-animate="$x"')

DatastarAttributes.custom_validity(expression)

Set a custom validity message. Datastar Pro only.

python
data.custom_validity("$msg")
# Attrs(' data-custom-validity="$msg"')

DatastarAttributes.match_media(signal_name, expression)

Sync a signal with matchMedia. Datastar Pro only.

python
data.match_media("is_dark", "'prefers-color-scheme: dark'")
# Attrs(' data-match-media:is-dark__case.snake="'prefers-color-scheme: dark'"')

DatastarAttributes.on_raf(expression, *, throttle=None)

Run an expression on every animation frame. Datastar Pro only.

python
data.on_raf("draw()", throttle="100ms")
# Attrs(' data-on-raf__throttle.100ms="draw()"')

DatastarAttributes.on_resize(expression, *, debounce=None, throttle=None)

React to element resize. Datastar Pro only.

python
data.on_resize("layout()", debounce="50ms", throttle="100ms")
# Attrs(' data-on-resize__debounce.50ms__throttle.100ms="layout()"')

DatastarAttributes.persist(*, include=None, exclude=None, storage_key=None, session=False)

Persist signals. Datastar Pro only.

python
data.persist(include="draft", storage_key="prefs", session=True)
# Attrs(' data-persist:prefs__session="{'include':'draft'}"')

DatastarAttributes.query_string(*, include=None, exclude=None, filter_empty=False, history=False)

Sync signals with the URL. Datastar Pro only.

python
data.query_string(include="page", history=True)
# Attrs(' data-query-string__history="{'include':'page'}"')

DatastarAttributes.replace_url(expression)

Replace the current browser URL. Datastar Pro only.

python
data.replace_url("`/page/${$page}`")
# Attrs(' data-replace-url="`/page/${$page}`"')

DatastarAttributes.scroll_into_view(*, behavior=None, horizontal=None, vertical=None, focus=False)

Scroll this element into view. Datastar Pro only.

python
data.scroll_into_view(behavior="smooth", vertical="center", focus=True)
# Attrs(' data-scroll-into-view__smooth__vcenter__focus')

DatastarAttributes.view_transition(expression)

Set view-transition-name. Datastar Pro only.

python
data.view_transition("$id")
# Attrs(' data-view-transition="$id"')

Actions

Declarative @… strings for data-on:*. @get, @post, @put, @patch, and @delete share the same option surface (query string, signal filters, selector, headers, retry, requestCancellation, and related knobs). selector only applies when content_type="form". Values in payload are JavaScript expressions — wrap literals with string_literal(). Methods marked Datastar Pro in docstrings (clipboard, intl, fit) emit valid @… strings; the open-source bundle ignores them unless Pro is enabled.

ContentType, RequestCancellation, and Retry are Literal aliases in stario.datastar.actions.

DatastarActions.get(url, queries=None, *, content_type=None, include=None, exclude=None, selector=None, headers=None, open_when_hidden=None, payload=None, retry=None, retry_interval_ms=None, retry_scaler=None, retry_max_wait_ms=None, retry_max_count=None, request_cancellation=None)

Build @get(...).

python
h.Button(data.on("click", at.get("/items", {"q": "$query"})), "Search")

DatastarActions.post(url, queries=None, *, content_type=None, include=None, exclude=None, selector=None, headers=None, open_when_hidden=None, payload=None, retry=None, retry_interval_ms=None, retry_scaler=None, retry_max_wait_ms=None, retry_max_count=None, request_cancellation=None)

Build @post(...) with the same option surface as get.

DatastarActions.put(url, queries=None, *, content_type=None, include=None, exclude=None, selector=None, headers=None, open_when_hidden=None, payload=None, retry=None, retry_interval_ms=None, retry_scaler=None, retry_max_wait_ms=None, retry_max_count=None, request_cancellation=None)

Build @put(...) with the same option surface as get.

DatastarActions.patch(url, queries=None, *, content_type=None, include=None, exclude=None, selector=None, headers=None, open_when_hidden=None, payload=None, retry=None, retry_interval_ms=None, retry_scaler=None, retry_max_wait_ms=None, retry_max_count=None, request_cancellation=None)

Build @patch(...) with the same option surface as get.

DatastarActions.delete(url, queries=None, *, content_type=None, include=None, exclude=None, selector=None, headers=None, open_when_hidden=None, payload=None, retry=None, retry_interval_ms=None, retry_scaler=None, retry_max_wait_ms=None, retry_max_count=None, request_cancellation=None)

Build @delete(...) with the same option surface as get.

DatastarActions.peek(callable_expr)

Build @peek(expr) to read a value without subscribing.

DatastarActions.set_all(value, include=None, exclude=None)

Build @setAll(...) to assign matching signals in bulk.

DatastarActions.toggle_all(include=None, exclude=None)

Build @toggleAll(...) to flip matching boolean signals.

DatastarActions.clipboard(text, is_base64=False)

Build @clipboard(...). Datastar Pro only.

DatastarActions.intl(type, value, options=None, locale=None)

Build @intl(...). Datastar Pro only.

value is a JavaScript expression. String values inside options are emitted as JavaScript string literals; numbers and booleans pass through.

DatastarActions.fit(v, old_min, old_max, new_min, new_max, should_clamp=False, should_round=False)

Build @fit(...) to remap a numeric expression. Datastar Pro only.

SSE

SSE(w) binds Datastar’s event stream to one Writer. Call open() to send text/event-stream headers immediately, or let the first event open the stream. Each method appends one SSE frame. Do not mix SSE with a completed response or a non-SSE Content-Type already started on the same writer.

Typical handler flow:

python
from stario.datastar import SSE, read_signals
 
async def update(c, w):
    sig = await read_signals(c.req)
    sse = SSE(w)
    sse.patch_elements(view)
    sse.patch_signals({"count": int(sig.get("count", 0)) + 1})

patch_elements accepts bytes, text, or Stario markup trees. For targeted morphs, pass selector= and optionally mode= (inner, replace, prepend, append, before, after; default omits mode for outer morph). Use namespace="svg" or "mathml" for non-HTML fragments. patch_signals takes a Mapping of JSON-serializable values with snakecase top-level keys. navigate performs client-side navigation (not an HTTP redirect; use responses.redirect for 3xx). `executescript appends trusted JavaScript via a temporary <script> element. remove` deletes nodes by CSS selector.

SSE.open()

Send text/event-stream headers now, before the first event.

SSE.patch_elements(content, *, mode=None, selector=None, namespace=None, view_transition=False, view_transition_selector=None)

Patch DOM elements with bytes, text, or Stario markup.

SSE.patch_signals(payload, *, only_if_missing=False)

Patch Datastar signals from a JSON-shaped mapping.

SSE.navigate(url)

Navigate the browser from an SSE response; not an HTTP redirect.

SSE.execute_script(code, *, auto_remove=True)

Run trusted developer-authored JavaScript by appending a script element.

The code is streamed verbatim; do not interpolate untrusted input.

SSE.remove(selector)

Remove nodes matching selector.