stario.datastar is the Python side of the Datastar browser client. You emit data-* attributes with data, write @ actions with at, read what the page sent with read_signals, and push updates with SSE. Attribute names and client behaviour are Datastar's; the upstream reference is the authority on those. This page is the Python surface.
from stario.datastar import SSE, FileSignal, ModuleScript, at, data, read_signalsSignal names use Python snake_case on the wire. Nested state goes in nested JSON objects, not dotted keys.
Loading the client#
ModuleScript() returns the <script type="module"> that loads Datastar. Put it in <head>. It defaults to a pinned CDN build. Pass src= to self-host, which you should do in production.
ModuleScript(src='https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/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())Attributes#
data is the stock DatastarAttributes instance. Each method returns an Attrs fragment: pre-rendered attribute text you pass to a tag like any other attribute, before the children.
from stario import Routefrom stario.datastar import at, datafrom stario import h ADD = Route("POST /cart") h.Button(data.on("click", at.post(ADD.href())), "Add")h.Input(data.bind("email"), {"type": "email"})Singular helpers (attr, class_, style, signal, computed) set one key. Plural helpers (attrs, classes, styles, signals, computeds) set several at once. Options use None to omit a key and let the client apply its default.
A custom Datastar bundle with another prefix needs another instance: DatastarAttributes("data-star-").
Hold state#
Signals are the page's state. signals sets several, with if_missing=True to keep values the client already holds. bind ties a form field to a signal in both directions. computed derives one signal from others. ref names an element so expressions can reach it.
DatastarAttributes.signals(payload, *, if_missing=False)#
Patch several signals.
data.signals({"count": 0, "open": False})# Attrs(" data-signals='{"count":0,"open":false}'")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.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')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.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.ref(signal_name)#
Store the current element in a signal.
data.ref("search_input")# Attrs(' data-ref="search_input"')Show state#
These bind an element's text, visibility, classes, styles, or attributes to an expression. indicator sets a boolean signal to true while a request from this element is in flight.
DatastarAttributes.text(expression)#
Bind text content to a Datastar expression.
data.text("$greeting")# Attrs(' data-text="$greeting"')DatastarAttributes.show(expression)#
Show or hide an element from a boolean expression.
data.show("$error != null")# Attrs(' data-show="$error != null"')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.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.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.indicator(signal_name)#
Track in-flight fetch state in a signal.
data.indicator("saving")# Attrs(' data-indicator="saving"')React to events#
on runs an expression on a DOM event; the expression is usually an at.* action. init runs once when the element mounts, which is where a page opens its subscribe stream. effect re-runs when the signals it reads change. The on_* helpers cover intersection, intervals, and signal patches.
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.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.effect(expression)#
Run a side effect when the element initializes or updates.
data.effect("el.focus()")# Attrs(' data-effect="el.focus()"')DatastarAttributes.on_intersect(expression, *, threshold=None, once=False, exit=False, delay=None, debounce=None, throttle=None, view_transition=False)#
React to viewport intersection.
data.on_intersect("load()", threshold=0.25, once=True, view_transition=True)# Attrs(' data-on-intersect__threshold.25__once__viewtransition="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'}"')Control the morph#
When the server patches HTML, Datastar morphs it into the live tree. These attributes tell the morph to leave an element alone or to keep one attribute as the browser has it.
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.preserve_attr(attrs)#
Preserve selected attributes during DOM morphing.
data.preserve_attr(["data-testid", "id"])# Attrs(' data-preserve-attr="data-testid id"')Datastar Pro#
These helpers emit the same attribute names as upstream Datastar Pro. The open-source bundle ignores them.
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#
at is the stock DatastarActions instance. Each method returns an action string such as @post('/cart'), which you place inside data.on(...) or data.init(...). Build the URL with route.href(); query and fragment belong on href(), not on the action.
Requests#
get, post, put, patch, delete, and query share one set of options: which signals to send, a selector for form content, extra headers, retry, and request cancellation. The response is an SSE stream that Datastar applies to the page. Values in payload are JavaScript expressions; wrap literal text with string_literal() from stario.datastar.format.
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.query(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 @query(...) with the same option surface as get.
Sends a QUERY request with signals in the body, like post.
Signals in the browser#
These act on signals without a request.
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.
Datastar Pro#
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.
Reading what the page sent#
Every Datastar request carries the page's signals as one JSON object. read_signals(req) returns it as a dict. GET and DELETE carry it in the datastar query parameter; other methods carry it in the body. Treat the result as untrusted input. Parsing goes through stario.json.
A file input bound with data.bind arrives as a signal too, not as multipart/form-data. FileSignal is the TypedDict for one such value: name, base64 contents, and mime, which may be None.
from stario.datastar import FileSignal, read_signals async def upload(c, w): signals = await read_signals(c.req) avatar: FileSignal = signals["avatar"]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 import Routefrom stario.datastar import SSE, read_signals ACTION = Route("POST", "/action") async def action(c, w): sig = await read_signals(c.req) SSE(w).patch_signals({"n": int(sig.get("n", 0)) + 1})SSE#
SSE(w) binds the Datastar event stream to one Writer. Create one per response. Each method appends one event. The first event sends the text/event-stream headers; call open() to send them earlier. Do not start an SSE stream on a writer that has already sent a response, and do not call a w.* response method after it.
from stario.datastar import SSE, read_signals async def increment(c, w): signals = await read_signals(c.req) sse = SSE(w) sse.patch_signals({"count": int(signals.get("count", 0)) + 1})patch_elements accepts bytes, text, or a markup tree. mode defaults to outer, which morphs the matched element. The others are inner, replace, prepend, append, before, and after. Without selector, elements match by id. navigate moves the browser from inside a stream by sending a short script, the same way the official Datastar client SDKs redirect. A 3xx status on a Datastar request does not navigate: fetch follows it and Datastar morphs the final page into the current one. Use w.redirect only for requests the browser itself makes, such as a plain form post. execute_script streams JavaScript you wrote; never interpolate user input into it.
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.remove(selector)#
Remove nodes matching selector.
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.
By the way#
stario.datastar.formatholds the wire helpersCase,js_object, andstring_literal.js_objecttreats string values as JavaScript expressions.JSEventis a type hint for the event namesdata.onaccepts. At runtime any string works.ContentType,RequestCancellation, andRetryareLiteralaliases instario.datastar.actions.
See also: the Datastar from Python how-to for the common patterns, and Live pages for why a page holds one stream open.