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.
from stario.datastar import data, at, SSE, ModuleScript, read_signals, FileSignalFor 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.
from stario.datastar import at, datafrom 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:
from stario.datastar.format import Case, js_object, string_literaljs_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.
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.
from stario.datastar import ModuleScriptfrom 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.
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.
data.attr("title", "$item.label")# Attrs(' data-attr:title="$item.label"')DatastarAttributes.attrs(mapping)
Set several HTML attributes from a mapping of expressions.
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.
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.
data.class_("hidden", "!$expanded")# Attrs(' data-class:hidden="!$expanded"')DatastarAttributes.classes(mapping)
Toggle several CSS classes from a mapping of expressions.
data.classes({"loading": "$pending"})# Attrs(' data-class="{'loading':$pending}"')DatastarAttributes.computed(key, expression)
Create one computed signal from a reactive expression.
data.computed("full_name", "$first + $last")# Attrs(' data-computed:full-name__case.snake="$first + $last"')DatastarAttributes.computeds(mapping)
Create several computed signals from expressions.
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.
data.effect("el.focus()")# Attrs(' data-effect="el.focus()"')DatastarAttributes.ignore(self_only=False)
Skip Datastar processing for this element or subtree.
data.ignore()# Attrs(' data-ignore') data.ignore(self_only=True)# Attrs(' data-ignore__self')DatastarAttributes.ignore_morph()
Prevent backend patches from morphing this subtree.
data.ignore_morph()# Attrs(' data-ignore-morph')DatastarAttributes.indicator(signal_name)
Track in-flight fetch state in a signal.
data.indicator("saving")# Attrs(' data-indicator="saving"')DatastarAttributes.init(expression, *, delay=None, view_transition=False)
Run an expression on element initialization.
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.
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.
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.
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.
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.
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.
data.preserve_attr(["data-testid", "id"])# Attrs(' data-preserve-attr="data-testid id"')DatastarAttributes.ref(signal_name)
Store the current element in a signal.
data.ref("search_input")# Attrs(' data-ref="search_input"')DatastarAttributes.show(expression)
Show or hide an element from a boolean expression.
data.show("$error != null")# Attrs(' data-show="$error != null"')DatastarAttributes.signal(name, expression, *, if_missing=False)
Patch one signal from a Datastar expression.
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.
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.
data.style("width", "$pct + '%'")# Attrs(' data-style:width="$pct + '%'"')DatastarAttributes.styles(mapping)
Set several inline style properties from expressions.
data.styles({"opacity": "$visible ? '1' : '0'"})# Attrs(' data-style="{'opacity':$visible ? '1' : '0'}"')DatastarAttributes.text(expression)
Bind text content to a Datastar expression.
data.text("$greeting")# Attrs(' data-text="$greeting"')DatastarAttributes.animate(expression)
Animate element attributes over time. Datastar Pro only.
data.animate("$x")# Attrs(' data-animate="$x"')DatastarAttributes.custom_validity(expression)
Set a custom validity message. Datastar Pro only.
data.custom_validity("$msg")# Attrs(' data-custom-validity="$msg"')DatastarAttributes.match_media(signal_name, expression)
Sync a signal with matchMedia. Datastar Pro only.
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.
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.
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.
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.
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.
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.
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.
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(...).
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:
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.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.