Guide

Getting Started

Datastar simplifies frontend development, allowing you to build backend-driven, interactive UIs using a hypermedia-first approach that extends and enhances HTML.

Datastar provides backend reactivity like htmx and frontend reactivity like Alpine.js in a lightweight frontend framework that doesn’t require any npm packages or other dependencies. It provides two primary functions:

  1. Modify the DOM and state by sending events from your backend.
  2. Build reactivity into your frontend using standard data-* HTML attributes.
Other useful resources include an AI-generated deep wiki, LLM-ingestible code samples, and single-page docs.

Installation #

The quickest way to use Datastar is to include it using a script tag that fetches it from a CDN.

1<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js"></script>

If you prefer to host the file yourself, download the script or create your own bundle using the bundler, then include it from the appropriate path.

1<script type="module" src="/path/to/datastar.js"></script>

To import Datastar using a package manager such as npm, Deno, or Bun, you can use an import statement.

1// @ts-expect-error (only required for TypeScript projects)
2import 'https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js'

data-* #

At the core of Datastar are data-* HTML attributes (hence the name). They allow you to add reactivity to your frontend and interact with your backend in a declarative way.

The Datastar VSCode extension and IntelliJ plugin provide autocompletion for all available data-* attributes.

The data-on attribute can be used to attach an event listener to an element and execute an expression whenever the event is triggered. The value of the attribute is a Datastar expression in which JavaScript can be used.

1<button data-on:click="alert('I’m sorry, Dave. I’m afraid I can’t do that.')">
2    Open the pod bay doors, HAL.
3</button>
Demo

We’ll explore more data attributes in the next section of the guide.

Patching Elements #

With Datastar, the backend drives the frontend by patching (adding, updating and removing) HTML elements in the DOM.

Datastar receives elements from the backend and manipulates the DOM using a morphing strategy (by default). Morphing ensures that only modified parts of the DOM are updated, preserving state and improving performance.

Datastar provides actions for sending requests to the backend. The @get() action sends a GET request to the provided URL using a fetch request.

1<button data-on:click="@get('/endpoint')">
2    Open the pod bay doors, HAL.
3</button>
4<div id="hal"></div>
Actions in Datastar are helper functions that have the syntax @actionName(). Read more about actions in the reference.

If the response has a content-type of text/html, the top-level HTML elements will be morphed into the existing DOM based on the element IDs.

1<div id="hal">
2    I’m sorry, Dave. I’m afraid I can’t do that.
3</div>

We call this a “Patch Elements” event because multiple elements can be patched into the DOM at once.

Demo
Waiting for an order...

In the example above, the DOM must contain an element with a hal ID in order for morphing to work. Other patching strategies are available, but morph is the best and simplest choice in most scenarios.

If the response has a content-type of text/event-stream, it can contain zero or more SSE events. The example above can be replicated using a datastar-patch-elements SSE event.

1event: datastar-patch-elements
2data: elements <div id="hal">
3data: elements     I’m sorry, Dave. I’m afraid I can’t do that.
4data: elements </div>
5

Because we can send as many events as we want in a stream, and because it can be a long-lived connection, we can extend the example above to first send HAL’s response and then, after a few seconds, reset the text.

 1event: datastar-patch-elements
 2data: elements <div id="hal">
 3data: elements     I’m sorry, Dave. I’m afraid I can’t do that.
 4data: elements </div>
 5
 6event: datastar-patch-elements
 7data: elements <div id="hal">
 8data: elements     Waiting for an order...
 9data: elements </div>
10
Demo
Waiting for an order...

Here’s the code to generate the SSE events above using the SDKs.

 1;; Import the SDK's api and your adapter
 2(require
 3 '[starfederation.datastar.clojure.api :as d*]
 4 '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])
 5
 6;; in a ring handler
 7(defn handler [request]
 8  ;; Create an SSE response
 9  (->sse-response request
10                  {on-open
11                   (fn [sse]
12                     ;; Patches elements into the DOM
13                     (d*/patch-elements! sse
14                                         "<div id=\"hal\">I’m sorry, Dave. I’m afraid I can’t do that.</div>")
15                     (Thread/sleep 1000)
16                     (d*/patch-elements! sse
17                                         "<div id=\"hal\">Waiting for an order...</div>"))}))
 1using StarFederation.Datastar.DependencyInjection;
 2
 3// Adds Datastar as a service
 4builder.Services.AddDatastar();
 5
 6app.MapGet("/", async (IDatastarService datastarService) =>
 7{
 8    // Patches elements into the DOM.
 9    await datastarService.PatchElementsAsync(@"<div id=""hal"">I’m sorry, Dave. I’m afraid I can’t do that.</div>");
10
11    await Task.Delay(TimeSpan.FromSeconds(1));
12
13    await datastarService.PatchElementsAsync(@"<div id=""hal"">Waiting for an order...</div>");
14});
 1import (
 2    "github.com/starfederation/datastar-go/datastar"
 3    time
 4)
 5
 6// Creates a new `ServerSentEventGenerator` instance.
 7sse := datastar.NewSSE(w,r)
 8
 9// Patches elements into the DOM.
10sse.PatchElements(
11    `<div id="hal">I’m sorry, Dave. I’m afraid I can’t do that.</div>`
12)
13
14time.Sleep(1 * time.Second)
15
16sse.PatchElements(
17    `<div id="hal">Waiting for an order...</div>`
18)
 1import starfederation.datastar.utils.ServerSentEventGenerator;
 2
 3// Creates a new `ServerSentEventGenerator` instance.
 4AbstractResponseAdapter responseAdapter = new HttpServletResponseAdapter(response);
 5ServerSentEventGenerator generator = new ServerSentEventGenerator(responseAdapter);
 6
 7// Patches elements into the DOM.
 8generator.send(PatchElements.builder()
 9    .data("<div id=\"hal\">I’m sorry, Dave. I’m afraid I can’t do that.</div>")
10    .build()
11);
12
13Thread.sleep(1000);
14
15generator.send(PatchElements.builder()
16    .data("<div id=\"hal\">Waiting for an order...</div>")
17    .build()
18);
 1val generator = ServerSentEventGenerator(response)
 2
 3generator.patchElements(
 4    elements = """<div id="hal">I’m sorry, Dave. I’m afraid I can’t do that.</div>""",
 5)
 6
 7Thread.sleep(ONE_SECOND)
 8
 9generator.patchElements(
10    elements = """<div id="hal">Waiting for an order...</div>""",
11)
 1use starfederation\datastar\ServerSentEventGenerator;
 2
 3// Creates a new `ServerSentEventGenerator` instance.
 4$sse = new ServerSentEventGenerator();
 5
 6// Patches elements into the DOM.
 7$sse->patchElements(
 8    '<div id="hal">I’m sorry, Dave. I’m afraid I can’t do that.</div>'
 9);
10
11sleep(1);
12
13$sse->patchElements(
14    '<div id="hal">Waiting for an order...</div>'
15);
1from datastar_py import ServerSentEventGenerator as SSE
2from datastar_py.sanic import datastar_response
3
4@app.get('/open-the-bay-doors')
5@datastar_response
6async def open_doors(request):
7    yield SSE.patch_elements('<div id="hal">I’m sorry, Dave. I’m afraid I can’t do that.</div>')
8    await asyncio.sleep(1)
9    yield SSE.patch_elements('<div id="hal">Waiting for an order...</div>')
 1require 'datastar'
 2
 3# Create a Datastar::Dispatcher instance
 4
 5datastar = Datastar.new(request:, response:)
 6
 7# In a Rack handler, you can instantiate from the Rack env
 8# datastar = Datastar.from_rack_env(env)
 9
10# Start a streaming response
11datastar.stream do |sse|
12  # Patches elements into the DOM.
13  sse.patch_elements %(<div id="hal">I’m sorry, Dave. I’m afraid I can’t do that.</div>)
14
15  sleep 1
16  
17  sse.patch_elements %(<div id="hal">Waiting for an order...</div>)
18end
 1use async_stream::stream;
 2use datastar::prelude::*;
 3use std::thread;
 4use std::time::Duration;
 5
 6Sse(stream! {
 7    // Patches elements into the DOM.
 8    yield PatchElements::new("<div id='hal'>I’m sorry, Dave. I’m afraid I can’t do that.</div>").into();
 9
10    thread::sleep(Duration::from_secs(1));
11    
12    yield PatchElements::new("<div id='hal'>Waiting for an order...</div>").into();
13})
1// Creates a new `ServerSentEventGenerator` instance (this also sends required headers)
2ServerSentEventGenerator.stream(req, res, (stream) => {
3    // Patches elements into the DOM.
4    stream.patchElements(`<div id="hal">I’m sorry, Dave. I’m afraid I can’t do that.</div>`);
5
6    setTimeout(() => {
7        stream.patchElements(`<div id="hal">Waiting for an order...</div>`);
8    }, 1000);
9});
In addition to your browser’s dev tools, the Datastar Inspector can be used to monitor and inspect SSE events received by Datastar.

We’ll cover event streams and SSE events in more detail later in the guide, but as you can see, they are just plain text events with a special syntax, made simpler by the SDKs.

Reactive Signals

In a hypermedia approach, the backend drives state to the frontend and acts as the primary source of truth. It’s up to the backend to determine what actions the user can take next by patching appropriate elements in the DOM.

Sometimes, however, you may need access to frontend state that’s driven by user interactions. Click, input and keydown events are some of the more common user events that you’ll want your frontend to be able to react to.

Datastar uses signals to manage frontend state. You can think of signals as reactive variables that automatically track and propagate changes in and to Datastar expressions. Signals are denoted using the $ prefix.

Data Attributes #

Datastar allows you to add reactivity to your frontend and interact with your backend in a declarative way using data-* attributes.

The Datastar VSCode extension and IntelliJ plugin provide autocompletion for all available data-* attributes.

data-bind #

The data-bind attribute sets up two-way data binding on any HTML element that receives user input or selections. These include input, textarea, select, checkbox and radio elements, as well as web components whose value can be made reactive.

1<input data-bind:foo />

This creates a new signal that can be called using $foo, and binds it to the element’s value. If either is changed, the other automatically updates.

You can accomplish the same thing passing the signal name as a value, an alternate syntax that might be more useful for some templating languages:

1<input data-bind="foo" />

data-text #

The data-text attribute sets the text content of an element to the value of a signal. The $ prefix is required to denote a signal.

1<input data-bind:foo />
2<div data-text="$foo"></div>
Demo

The value of the data-text attribute is a Datastar expression that is evaluated, meaning that we can use JavaScript in it.

1<input data-bind:foo />
2<div data-text="$foo.toUpperCase()"></div>
Demo

data-computed #

The data-computed attribute creates a new signal that is derived from a reactive expression. The computed signal is read-only, and its value is automatically updated when any signals in the expression are updated.

1<input data-bind:foo />
2<div data-computed:repeated="$foo.repeat(2)" data-text="$repeated"></div>

This results in the $repeated signal’s value always being equal to the value of the $foo signal repeated twice. Computed signals are useful for memoizing expressions containing other signals.

Demo

data-show #

The data-show attribute can be used to show or hide an element based on whether an expression evaluates to true or false.

1<input data-bind:foo />
2<button data-show="$foo != ''">
3    Save
4</button>

This results in the button being visible only when the input value is not an empty string. This could also be shortened to data-show="$foo".

Demo

Since the button is visible until Datastar processes the data-show attribute, it’s a good idea to set its initial style to display: none; to prevent a flash of unwanted content.

1<input data-bind:foo />
2<button data-show="$foo != ''" style="display: none;">
3    Save
4</button>

data-class #

The data-class attribute allows us to add or remove an element’s class based on an expression.

1<input data-bind:foo />
2<button data-class:success="$foo != ''">
3    Save
4</button>

If the expression evaluates to true, the success class is added to the element; otherwise, it is removed.

Demo

The data-class attribute can also be used to add or remove multiple classes from an element using a set of key-value pairs, where the keys represent class names and the values represent expressions.

1<button data-class="{success: $foo != '', 'font-bold': $foo == 'bar'}">
2    Save
3</button>

data-attr #

The data-attr attribute can be used to bind the value of any HTML attribute to an expression.

1<input data-bind:foo />
2<button data-attr:disabled="$foo == ''">
3    Save
4</button>

This results in a disabled attribute being given the value true whenever the input is an empty string.

Demo

The data-attr attribute can also be used to set the values of multiple attributes on an element using a set of key-value pairs, where the keys represent attribute names and the values represent expressions.

1<button data-attr="{disabled: $foo == '', title: $foo}">Save</button>

data-signals #

Signals are globally accessible from anywhere in the DOM. So far, we’ve created signals on the fly using data-bind and data-computed. If a signal is used without having been created, it will be created automatically and its value set to an empty string.

Another way to create signals is using the data-signals attribute, which patches (adds, updates or removes) one or more signals into the existing signals.

1<div data-signals:foo="1"></div>

Signals can be nested using dot-notation.

1<div data-signals:form.foo="2"></div>

The data-signals attribute can also be used to patch multiple signals using a set of key-value pairs, where the keys represent signal names and the values represent expressions.

1<div data-signals="{foo: 1, form: {foo: 2}}"></div>

data-on #

The data-on attribute can be used to attach an event listener to an element and run an expression whenever the event is triggered.

1<input data-bind:foo />
2<button data-on:click="$foo = ''">
3    Reset
4</button>

This results in the $foo signal’s value being set to an empty string whenever the button element is clicked. This can be used with any valid event name such as data-on:keydown, data-on:mouseover, etc. Custom events may also be used.

Demo

These are just some of the attributes available in Datastar. For a complete list, see the attribute reference.

Frontend Reactivity #

Datastar’s data attributes enable declarative signals and expressions, providing a simple yet powerful way to add reactivity to the frontend.

Datastar expressions are strings that are evaluated by Datastar attributes and actions. While they are similar to JavaScript, there are some important differences that are explained in the next section of the guide.

1<div data-signals:hal="'...'">
2    <button data-on:click="$hal = 'Affirmative, Dave. I read you.'">
3        HAL, do you read me?
4    </button>
5    <div data-text="$hal"></div>
6</div>
Demo

See if you can figure out what the code below does based on what you’ve learned so far, before trying the demo below it.

 1<div
 2    data-signals="{response: '', answer: 'bread'}"
 3    data-computed:correct="$response.toLowerCase() == $answer"
 4>
 5    <div id="question">What do you put in a toaster?</div>
 6    <button data-on:click="$response = prompt('Answer:') ?? ''">BUZZ</button>
 7    <div data-show="$response != ''">
 8        You answered “<span data-text="$response"></span>”.
 9        <span data-show="$correct">That is correct ✅</span>
10        <span data-show="!$correct">
11        The correct answer is “
12        <span data-text="$answer"></span>
13        ” 🤷
14        </span>
15    </div>
16</div>
Demo

What do you put in a toaster?

You answered “”. That is correct ✅ The correct answer is “bread” 🤷

The Datastar Inspector can be used to inspect and filter current signals and view signal patch events in real-time.

Patching Signals #

Remember that in a hypermedia approach, the backend drives state to the frontend. Just like with elements, frontend signals can be patched (added, updated and removed) from the backend using backend actions.

1<div data-signals:hal="'...'">
2    <button data-on:click="@get('/endpoint')">
3        HAL, do you read me?
4    </button>
5    <div data-text="$hal"></div>
6</div>

If a response has a content-type of application/json, the signal values are patched into the frontend signals.

We call this a “Patch Signals” event because multiple signals can be patched (using JSON Merge Patch RFC 7396) into the existing signals.

1{"hal": "Affirmative, Dave. I read you."}
Demo

If the response has a content-type of text/event-stream, it can contain zero or more SSE events. The example above can be replicated using a datastar-patch-signals SSE event.

1event: datastar-patch-signals
2data: signals {hal: 'Affirmative, Dave. I read you.'}
3

Because we can send as many events as we want in a stream, and because it can be a long-lived connection, we can extend the example above to first set the hal signal to an “affirmative” response and then, after a second, reset the signal.

1event: datastar-patch-signals
2data: signals {hal: 'Affirmative, Dave. I read you.'}
3
4// Wait 1 second
5
6event: datastar-patch-signals
7data: signals {hal: '...'}
8
Demo

Here’s the code to generate the SSE events above using the SDKs.

 1;; Import the SDK's api and your adapter
 2(require
 3  '[starfederation.datastar.clojure.api :as d*]
 4  '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])
 5
 6;; in a ring handler
 7(defn handler [request]
 8  ;; Create an SSE response
 9  (->sse-response request
10                  {on-open
11                   (fn [sse]
12                     ;; Patches signal.
13                     (d*/patch-signals! sse "{hal: 'Affirmative, Dave. I read you.'}")
14                     (Thread/sleep 1000)
15                     (d*/patch-signals! sse "{hal: '...'}"))}))
 1using StarFederation.Datastar.DependencyInjection;
 2
 3// Adds Datastar as a service
 4builder.Services.AddDatastar();
 5
 6app.MapGet("/hal", async (IDatastarService datastarService) =>
 7{
 8    // Patches signals.
 9    await datastarService.PatchSignalsAsync(new { hal = "Affirmative, Dave. I read you" });
10
11    await Task.Delay(TimeSpan.FromSeconds(3));
12
13    await datastarService.PatchSignalsAsync(new { hal = "..." });
14});
 1import (
 2    "github.com/starfederation/datastar-go/datastar"
 3)
 4
 5// Creates a new `ServerSentEventGenerator` instance.
 6sse := datastar.NewSSE(w, r)
 7
 8// Patches signals
 9sse.PatchSignals([]byte(`{hal: 'Affirmative, Dave. I read you.'}`))
10
11time.Sleep(1 * time.Second)
12
13sse.PatchSignals([]byte(`{hal: '...'}`))
 1import starfederation.datastar.utils.ServerSentEventGenerator;
 2
 3// Creates a new `ServerSentEventGenerator` instance.
 4AbstractResponseAdapter responseAdapter = new HttpServletResponseAdapter(response);
 5ServerSentEventGenerator generator = new ServerSentEventGenerator(responseAdapter);
 6
 7// Patches signals.
 8generator.send(PatchSignals.builder()
 9    .data("{\"hal\": \"Affirmative, Dave. I read you.\"}")
10    .build()
11);
12
13Thread.sleep(1000);
14
15generator.send(PatchSignals.builder()
16    .data("{\"hal\": \"...\"}")
17    .build()
18);
 1val generator = ServerSentEventGenerator(response)
 2
 3generator.patchSignals(
 4    signals = """{"hal": "Affirmative, Dave. I read you."}""",
 5)
 6
 7Thread.sleep(ONE_SECOND)
 8
 9generator.patchSignals(
10    signals = """{"hal": "..."}""",
11)
 1use starfederation\datastar\ServerSentEventGenerator;
 2
 3// Creates a new `ServerSentEventGenerator` instance.
 4$sse = new ServerSentEventGenerator();
 5
 6// Patches signals.
 7$sse->patchSignals(['hal' => 'Affirmative, Dave. I read you.']);
 8
 9sleep(1);
10
11$sse->patchSignals(['hal' => '...']);
1from datastar_py import ServerSentEventGenerator as SSE
2from datastar_py.sanic import datastar_response
3
4@app.get('/do-you-read-me')
5@datastar_response
6async def open_doors(request):
7    yield SSE.patch_signals({"hal": "Affirmative, Dave. I read you."})
8    await asyncio.sleep(1)
9    yield SSE.patch_signals({"hal": "..."})
 1require 'datastar'
 2
 3# Create a Datastar::Dispatcher instance
 4
 5datastar = Datastar.new(request:, response:)
 6
 7# In a Rack handler, you can instantiate from the Rack env
 8# datastar = Datastar.from_rack_env(env)
 9
10# Start a streaming response
11datastar.stream do |sse|
12  # Patches signals
13  sse.patch_signals(hal: 'Affirmative, Dave. I read you.')
14
15  sleep 1
16  
17  sse.patch_signals(hal: '...')
18end
 1use async_stream::stream;
 2use datastar::prelude::*;
 3use std::thread;
 4use std::time::Duration;
 5
 6Sse(stream! {
 7    // Patches signals.
 8    yield PatchSignals::new("{hal: 'Affirmative, Dave. I read you.'}").into();
 9
10    thread::sleep(Duration::from_secs(1));
11    
12    yield PatchSignals::new("{hal: '...'}").into();
13})
1// Creates a new `ServerSentEventGenerator` instance (this also sends required headers)
2ServerSentEventGenerator.stream(req, res, (stream) => {
3    // Patches signals.
4    stream.patchSignals({'hal': 'Affirmative, Dave. I read you.'});
5
6    setTimeout(() => {
7        stream.patchSignals({'hal': '...'});
8    }, 1000);
9});
In addition to your browser’s dev tools, the Datastar Inspector can be used to monitor and inspect SSE events received by Datastar.

We’ll cover event streams and SSE events in more detail later in the guide, but as you can see, they are just plain text events with a special syntax, made simpler by the SDKs.

Datastar Expressions

Datastar expressions are strings that are evaluated by data-* attributes. While they are similar to JavaScript, there are some important differences that make them more powerful for declarative hypermedia applications.

Datastar Expressions #

The following example outputs 1 because we’ve defined foo as a signal with the initial value 1, and are using $foo in a data-* attribute.

1<div data-signals:foo="1">
2    <div data-text="$foo"></div>
3</div>

A variable el is available in every Datastar expression, representing the element that the attribute is attached to.

1<div id="foo" data-text="el.id"></div>

When Datastar evaluates the expression $foo, it first converts it to the signal value, and then evaluates that expression in a sandboxed context. This means that JavaScript can be used in Datastar expressions.

1<div data-text="$foo.length"></div>

JavaScript operators are also available in Datastar expressions. This includes (but is not limited to) the ternary operator ?:, the logical OR operator ||, and the logical AND operator &&. These operators are helpful in keeping Datastar expressions terse.

 1// Output one of two values, depending on the truthiness of a signal
 2<div data-text="$landingGearRetracted ? 'Ready' : 'Waiting'"></div>
 3
 4// Show a countdown if the signal is truthy or the time remaining is less than 10 seconds
 5<div data-show="$landingGearRetracted || $timeRemaining < 10">
 6    Countdown
 7</div>
 8
 9// Only send a request if the signal is truthy
10<button data-on:click="$landingGearRetracted && @post('/launch')">
11    Launch
12</button>

Multiple statements can be used in a single expression by separating them with a semicolon.

1<div data-signals:foo="1">
2    <button data-on:click="$landingGearRetracted = true; @post('/launch')">
3        Force launch
4    </button>
5</div>

Expressions may span multiple lines, but a semicolon must be used to separate statements. Unlike JavaScript, line breaks alone are not sufficient to separate statements.

1<div data-signals:foo="1">
2    <button data-on:click="
3        $landingGearRetracted = true; 
4        @post('/launch')
5    ">
6        Force launch
7    </button>
8</div>

Using JavaScript #

Most of your JavaScript logic should go in data-* attributes, since reactive signals and actions only work in Datastar expressions.

Caution: if you find yourself trying to do too much in Datastar expressions, you are probably overcomplicating it™.

Any additional JavaScript functionality you require that cannot belong in data-* attributes should be extracted out into external scripts or, better yet, web components.

Always encapsulate state and send props down, events up.

External Scripts #

When using external scripts, pass data into functions via arguments and return a result or listen for custom events dispatched from them props down, events up.

In this way, the function is encapsulated – all it knows is that it receives input via an argument, acts on it, and optionally returns a result or dispatches a custom event – and data-* attributes can be used to drive reactivity.

1<div data-signals:result>
2    <input data-bind:foo 
3        data-on:input="$result = myfunction($foo)"
4    >
5    <span data-text="$result"></span>
6</div>
1function myfunction(data) {
2    return `You entered: ${data}`;
3}

If your function call is asynchronous then it will need to dispatch a custom event containing the result. While asynchronous code can be placed within Datastar expressions, Datastar will not await it.

1<div data-signals:result>
2    <input data-bind:foo 
3           data-on:input="myfunction(el, $foo)"
4           data-on:mycustomevent__window="$result = evt.detail.value"
5    >
6    <span data-text="$result"></span>
7</div>
1async function myfunction(element, data) {
2    const value = await new Promise((resolve) => {
3        setTimeout(() => resolve(`You entered: ${data}`), 1000);
4    });
5    element.dispatchEvent(
6        new CustomEvent('mycustomevent', {detail: {value}})
7    );
8}

See the sortable example.

Web Components #

Web components allow you create reusable, encapsulated, custom elements. They are native to the web and require no external libraries or frameworks. Web components unlock custom elements – HTML tags with custom behavior and styling.

When using web components, pass data into them via attributes and listen for custom events dispatched from them (props down, events up).

In this way, the web component is encapsulated – all it knows is that it receives input via an attribute, acts on it, and optionally dispatches a custom event containing the result – and data-* attributes can be used to drive reactivity.

1<div data-signals:result="''">
2    <input data-bind:foo />
3    <my-component
4        data-attr:src="$foo"
5        data-on:mycustomevent="$result = evt.detail.value"
6    ></my-component>
7    <span data-text="$result"></span>
8</div>
 1class MyComponent extends HTMLElement {
 2    static get observedAttributes() {
 3        return ['src'];
 4    }
 5
 6    attributeChangedCallback(name, oldValue, newValue) {
 7        const value = `You entered: ${newValue}`;
 8        this.dispatchEvent(
 9            new CustomEvent('mycustomevent', {detail: {value}})
10        );
11    }
12}
13
14customElements.define('my-component', MyComponent);

Since the value attribute is allowed on web components, it is also possible to use data-bind to bind a signal to the web component’s value. Note that a change event must be dispatched so that the event listener used by data-bind is triggered by the value change.

See the web component example.

Executing Scripts #

Just like elements and signals, the backend can also send JavaScript to be executed on the frontend using backend actions.

1<button data-on:click="@get('/endpoint')">
2    What are you talking about, HAL?
3</button>

If a response has a content-type of text/javascript, the value will be executed as JavaScript in the browser.

1alert('This mission is too important for me to allow you to jeopardize it.')
Demo

If the response has a content-type of text/event-stream, it can contain zero or more SSE events. The example above can be replicated by including a script tag inside of a datastar-patch-elements SSE event.

1event: datastar-patch-elements
2data: elements <div id="hal">
3data: elements     <script>alert('This mission is too important for me to allow you to jeopardize it.')</script>
4data: elements </div>
5

If you only want to execute a script, you can append the script tag to the body.

1event: datastar-patch-elements
2data: mode append
3data: selector body
4data: elements <script>alert('This mission is too important for me to allow you to jeopardize it.')</script>
5

Most SDKs have an ExecuteScript helper function for executing a script. Here’s the code to generate the SSE event above using the Go SDK.

1sse := datastar.NewSSE(writer, request)
2sse.ExecuteScript(`alert('This mission is too important for me to allow you to jeopardize it.')`)
Demo

We’ll cover event streams and SSE events in more detail later in the guide, but as you can see, they are just plain text events with a special syntax, made simpler by the SDKs.

Backend Requests

Between attributes and actions, Datastar provides you with everything you need to build hypermedia-driven applications. Using this approach, the backend drives state to the frontend and acts as the single source of truth, determining what actions the user can take next.

Sending Signals #

By default, all signals (except for local signals whose keys begin with an underscore) are sent in an object with every backend request. When using a GET request, the signals are sent as a datastar query parameter, otherwise they are sent as a JSON body.

By sending all signals in every request, the backend has full access to the frontend state. This is by design. It is not recommended to send partial signals, but if you must, you can use the filterSignals option to filter the signals sent to the backend.

Nesting Signals #

Signals can be nested, making it easier to target signals in a more granular way on the backend.

Using dot-notation:

1<div data-signals:foo.bar="1"></div>

Using object syntax:

1<div data-signals="{foo: {bar: 1}}"></div>

Using two-way binding:

1<input data-bind:foo.bar />

A practical use-case of nested signals is when you have repetition of state on a page. The following example tracks the open/closed state of a menu on both desktop and mobile devices, and the toggleAll() action to toggle the state of all menus at once.

1<div data-signals="{menu: {isOpen: {desktop: false, mobile: false}}}">
2    <button data-on:click="@toggleAll({include: /^menu\.isOpen\./})">
3        Open/close menu
4    </button>
5</div>

Reading Signals #

To read signals from the backend, JSON decode the datastar query param for GET requests, and the request body for all other methods.

All SDKs provide a helper function to read signals. Here’s how you would read the nested signal foo.bar from an incoming request.

No example found for Clojure

 1using StarFederation.Datastar.DependencyInjection;
 2
 3// Adds Datastar as a service
 4builder.Services.AddDatastar();
 5
 6public record Signals
 7{
 8    [JsonPropertyName("foo")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
 9    public FooSignals? Foo { get; set; } = null;
10
11    public record FooSignals
12    {
13        [JsonPropertyName("bar")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
14        public string? Bar { get; set; }
15    }
16}
17
18app.MapGet("/read-signals", async (IDatastarService datastarService) =>
19{
20    Signals? mySignals = await datastarService.ReadSignalsAsync<Signals>();
21    var bar = mySignals?.Foo?.Bar;
22});
 1import ("github.com/starfederation/datastar-go/datastar")
 2
 3type Signals struct {
 4    Foo struct {
 5        Bar string `json:"bar"`
 6    } `json:"foo"`
 7}
 8
 9signals := &Signals{}
10if err := datastar.ReadSignals(request, signals); err != nil {
11    http.Error(w, err.Error(), http.StatusBadRequest)
12    return
13}

No example found for Java

 1@Serializable
 2data class Signals(
 3    val foo: String,
 4)
 5
 6val jsonUnmarshaller: JsonUnmarshaller<Signals> = { json -> Json.decodeFromString(json) }
 7
 8val request: Request =
 9    postRequest(
10        body =
11            """
12            {
13                "foo": "bar"
14            }
15            """.trimIndent(),
16    )
17
18val signals = readSignals(request, jsonUnmarshaller)
1use starfederation\datastar\ServerSentEventGenerator;
2
3// Reads all signals from the request.
4$signals = ServerSentEventGenerator::readSignals();
1from datastar_py.fastapi import datastar_response, read_signals
2
3@app.get("/updates")
4@datastar_response
5async def updates(request: Request):
6    # Retrieve a dictionary with the current state of the signals from the frontend
7    signals = await read_signals(request)
1# Setup with request
2datastar = Datastar.new(request:, response:)
3
4# Read signals
5some_signal = datastar.signals[:some_signal]

No example found for Rust

No example found for TypeScript

SSE Events #

Datastar can stream zero or more Server-Sent Events (SSE) from the web server to the browser. There’s no special backend plumbing required to use SSE, just some special syntax. Fortunately, SSE is straightforward and provides us with some advantages, in addition to allowing us to send multiple events in a single response (in contrast to sending text/html or application/json responses).

First, set up your backend in the language of your choice. Familiarize yourself with sending SSE events, or use one of the backend SDKs to get up and running even faster. We’re going to use the SDKs in the examples below, which set the appropriate headers and format the events for us.

The following code would exist in a controller action endpoint in your backend.

 1;; Import the SDK's api and your adapter
 2(require
 3 '[starfederation.datastar.clojure.api :as d*]
 4 '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])
 5
 6;; in a ring handler
 7(defn handler [request]
 8  ;; Create an SSE response
 9  (->sse-response request
10                  {on-open
11                   (fn [sse]
12                     ;; Patches elements into the DOM
13                     (d*/patch-elements! sse
14                                         "<div id=\"question\">What do you put in a toaster?</div>")
15
16                     ;; Patches signals
17                     (d*/patch-signals! sse "{response: '', answer: 'bread'}"))}))
 1using StarFederation.Datastar.DependencyInjection;
 2
 3// Adds Datastar as a service
 4builder.Services.AddDatastar();
 5
 6app.MapGet("/", async (IDatastarService datastarService) =>
 7{
 8    // Patches elements into the DOM.
 9    await datastarService.PatchElementsAsync(@"<div id=""question"">What do you put in a toaster?</div>");
10
11    // Patches signals.
12    await datastarService.PatchSignalsAsync(new { response = "", answer = "bread" });
13});
 1import ("github.com/starfederation/datastar-go/datastar")
 2
 3// Creates a new `ServerSentEventGenerator` instance.
 4sse := datastar.NewSSE(w,r)
 5
 6// Patches elements into the DOM.
 7sse.PatchElements(
 8    `<div id="question">What do you put in a toaster?</div>`
 9)
10
11// Patches signals.
12sse.PatchSignals([]byte(`{response: '', answer: 'bread'}`))
 1import starfederation.datastar.utils.ServerSentEventGenerator;
 2
 3// Creates a new `ServerSentEventGenerator` instance.
 4AbstractResponseAdapter responseAdapter = new HttpServletResponseAdapter(response);
 5ServerSentEventGenerator generator = new ServerSentEventGenerator(responseAdapter);
 6
 7// Patches elements into the DOM.
 8generator.send(PatchElements.builder()
 9    .data("<div id=\"question\">What do you put in a toaster?</div>")
10    .build()
11);
12
13// Patches signals.
14generator.send(PatchSignals.builder()
15    .data("{\"response\": \"\", \"answer\": \"\"}")
16    .build()
17);
1val generator = ServerSentEventGenerator(response)
2
3generator.patchElements(
4    elements = """<div id="question">What do you put in a toaster?</div>""",
5)
6
7generator.patchSignals(
8    signals = """{"response": "", "answer": "bread"}""",
9)
 1use starfederation\datastar\ServerSentEventGenerator;
 2
 3// Creates a new `ServerSentEventGenerator` instance.
 4$sse = new ServerSentEventGenerator();
 5
 6// Patches elements into the DOM.
 7$sse->patchElements(
 8    '<div id="question">What do you put in a toaster?</div>'
 9);
10
11// Patches signals.
12$sse->patchSignals(['response' => '', 'answer' => 'bread']);
1from datastar_py import ServerSentEventGenerator as SSE
2from datastar_py.litestar import DatastarResponse
3
4async def endpoint():
5    return DatastarResponse([
6        SSE.patch_elements('<div id="question">What do you put in a toaster?</div>'),
7        SSE.patch_signals({"response": "", "answer": "bread"})
8    ])
 1require 'datastar'
 2
 3# Create a Datastar::Dispatcher instance
 4
 5datastar = Datastar.new(request:, response:)
 6
 7# In a Rack handler, you can instantiate from the Rack env
 8# datastar = Datastar.from_rack_env(env)
 9
10# Start a streaming response
11datastar.stream do |sse|
12  # Patches elements into the DOM
13  sse.patch_elements %(<div id="question">What do you put in a toaster?</div>)
14
15  # Patches signals
16  sse.patch_signals(response: '', answer: 'bread')
17end
 1use datastar::prelude::*;
 2use async_stream::stream;
 3
 4Sse(stream! {
 5    // Patches elements into the DOM.
 6    yield PatchElements::new("<div id='question'>What do you put in a toaster?</div>").into();
 7
 8    // Patches signals.
 9    yield PatchSignals::new("{response: '', answer: 'bread'}").into();
10})
1// Creates a new `ServerSentEventGenerator` instance (this also sends required headers)
2ServerSentEventGenerator.stream(req, res, (stream) => {
3      // Patches elements into the DOM.
4     stream.patchElements(`<div id="question">What do you put in a toaster?</div>`);
5
6     // Patches signals.
7     stream.patchSignals({'response':  '', 'answer': 'bread'});
8});

The PatchElements() function updates the provided HTML element into the DOM, replacing the element with id="question". An element with the ID question must already exist in the DOM.

The PatchSignals() function updates the response and answer signals into the frontend signals.

With our backend in place, we can now use the data-on:click attribute to trigger the @get() action, which sends a GET request to the /actions/quiz endpoint on the server when a button is clicked.

 1<div
 2    data-signals="{response: '', answer: ''}"
 3    data-computed:correct="$response.toLowerCase() == $answer"
 4>
 5    <div id="question"></div>
 6    <button data-on:click="@get('/actions/quiz')">Fetch a question</button>
 7    <button
 8        data-show="$answer != ''"
 9        data-on:click="$response = prompt('Answer:') ?? ''"
10    >
11        BUZZ
12    </button>
13    <div data-show="$response != ''">
14        You answered “<span data-text="$response"></span>”.
15        <span data-show="$correct">That is correct ✅</span>
16        <span data-show="!$correct">
17        The correct answer is “<span data-text="$answer"></span>” 🤷
18        </span>
19    </div>
20</div>

Now when the Fetch a question button is clicked, the server will respond with an event to modify the question element in the DOM and an event to modify the response and answer signals. We’re driving state from the backend!

Demo

...

You answered “”. That is correct ✅ The correct answer is “” 🤷

data-indicator #

The data-indicator attribute sets the value of a signal to true while the request is in flight, otherwise false. We can use this signal to show a loading indicator, which may be desirable for slower responses.

1<div id="question"></div>
2<button
3    data-on:click="@get('/actions/quiz')"
4    data-indicator:fetching
5>
6    Fetch a question
7</button>
8<div data-class:loading="$fetching" class="indicator"></div>
Demo

...

Indicator

Backend Actions #

We’re not limited to sending just GET requests. Datastar provides backend actions for each of the methods available: @get(), @post(), @put(), @patch() and @delete().

Here’s how we can send an answer to the server for processing, using a POST request.

1<button data-on:click="@post('/actions/quiz')">
2    Submit answer
3</button>

One of the benefits of using SSE is that we can send multiple events (patch elements and patch signals) in a single response.

1(d*/patch-elements! sse "<div id=\"question\">...</div>")
2(d*/patch-elements! sse "<div id=\"instructions\">...</div>")
3(d*/patch-signals! sse "{answer: '...', prize: '...'}")
1datastarService.PatchElementsAsync(@"<div id=""question"">...</div>");
2datastarService.PatchElementsAsync(@"<div id=""instructions"">...</div>");
3datastarService.PatchSignalsAsync(new { answer = "...", prize = "..." } );
1sse.PatchElements(`<div id="question">...</div>`)
2sse.PatchElements(`<div id="instructions">...</div>`)
3sse.PatchSignals([]byte(`{answer: '...', prize: '...'}`))
 1generator.send(PatchElements.builder()
 2    .data("<div id=\"question\">...</div>")
 3    .build()
 4);
 5generator.send(PatchElements.builder()
 6    .data("<div id=\"instructions\">...</div>")
 7    .build()
 8);
 9generator.send(PatchSignals.builder()
10    .data("{\"answer\": \"...\", \"prize\": \"...\"}")
11    .build()
12);
1generator.patchElements(
2    elements = """<div id="question">...</div>""",
3)
4generator.patchElements(
5    elements = """<div id="instructions">...</div>""",
6)
7generator.patchSignals(
8    signals = """{"answer": "...", "prize": "..."}""",
9)
1$sse->patchElements('<div id="question">...</div>');
2$sse->patchElements('<div id="instructions">...</div>');
3$sse->patchSignals(['answer' => '...', 'prize' => '...']);
1return DatastarResponse([
2    SSE.patch_elements('<div id="question">...</div>'),
3    SSE.patch_elements('<div id="instructions">...</div>'),
4    SSE.patch_signals({"answer": "...", "prize": "..."})
5])
1datastar.stream do |sse|
2  sse.patch_elements('<div id="question">...</div>')
3  sse.patch_elements('<div id="instructions">...</div>')
4  sse.patch_signals(answer: '...', prize: '...')
5end
1yield PatchElements::new("<div id='question'>...</div>").into()
2yield PatchElements::new("<div id='instructions'>...</div>").into()
3yield PatchSignals::new("{answer: '...', prize: '...'}").into()
1stream.patchElements('<div id="question">...</div>');
2stream.patchElements('<div id="instructions">...</div>');
3stream.patchSignals({'answer': '...', 'prize': '...'});
In addition to your browser’s dev tools, the Datastar Inspector can be used to monitor and inspect SSE events received by Datastar.

Read more about SSE events in the reference.

Congratulations #

You’ve actually read the entire guide! You should now know how to use Datastar to build reactive applications that communicate with the backend using backend requests and SSE events.

Feel free to dive into the reference and explore the examples next, to learn more about what you can do with Datastar.

The Tao of Datastar

Datastar is just a tool. The Tao of Datastar, or “the Datastar way” as it is often referred to, is a set of opinions from the core team on how to best use Datastar to build maintainable, scalable, high-performance web apps.

Ignore them at your own peril!

The Tao of Datastar

State in the Right Place #

Most state should live in the backend. Since the frontend is exposed to the user, the backend should be the source of truth for your application state.

Start with the Defaults #

The default configuration options are the recommended settings for the majority of applications. Start with the defaults, and before you ever get tempted to change them, stop and ask yourself, well ... how did I get here?

Patch Elements & Signals #

Since the backend is the source of truth, it should drive the frontend by patching (adding, updating and removing) HTML elements and signals.

Use Signals Sparingly #

Overusing signals typically indicates trying to manage state on the frontend. Favor fetching current state from the backend rather than pre-loading and assuming frontend state is current. A good rule of thumb is to only use signals for user interactions (e.g. toggling element visibility) and for sending new state to the backend (e.g. by binding signals to form input elements).

In Morph We Trust #

Morphing ensures that only modified parts of the DOM are updated, preserving state and improving performance. This allows you to send down large chunks of the DOM tree (all the way up to the html tag), sometimes known as “fat morph”, rather than trying to manage fine-grained updates yourself. If you want to explicitly ignore morphing an element, place the data-ignore-morph attribute on it.

SSE Responses #

SSE responses allow you to send 0 to n events, in which you can patch elements, patch signals, and execute scripts. Since event streams are just HTTP responses with some special formatting that SDKs can handle for you, there’s no real benefit to using a content type other than text/event-stream.

Compression #

Since SSE responses stream events from the backend and morphing allows sending large chunks of DOM, compressing the response is a natural choice. Compression ratios of 200:1 are not uncommon when compressing streams using Brotli. Read more about compressing streams in this article.

Backend Templating #

Since your backend generates your HTML, you can and should use your templating language to keep things DRY (Don’t Repeat Yourself).

Page navigation hasn't changed in 30 years. Use the anchor element (<a>) to navigate to a new page, or a redirect if redirecting from the backend. For smooth page transitions, use the View Transition API.

Browser History #

Browsers automatically keep a history of pages visited. As soon as you start trying to manage browser history yourself, you are adding complexity. Each page is a resource. Use anchor tags and let the browser do what it is good at.

CQRS #

CQRS, in which commands (writes) and requests (reads) are segregated, makes it possible to have a single long-lived request to receive updates from the backend (reads), while making multiple short-lived requests to the backend (writes). It is a powerful pattern that makes real-time collaboration simple using Datastar. Here’s a basic example.

1<div id="main" data-init="@get('/cqrs_endpoint')">
2    <button data-on:click="@post('/do_something')">
3        Do something
4    </button>
5</div>

Loading Indicators #

Loading indicators inform the user that an action is in progress. Use the data-indicator attribute to show loading indicators on elements that trigger backend requests. Here’s an example of a button that shows a loading element while waiting for a response from the backend.

1<div>
2    <button data-indicator:_loading
3            data-on:click="@post('/do_something')"
4    >
5        Do something
6        <span data-show="$_loading">Loading...</span>
7    </button>
8</div>

When using CQRS, it is generally better to manually show a loading indicator when backend requests are made, and allow it to be hidden when the DOM is updated from the backend. Here’s an example.

1<div>
2    <button data-on:click="el.classList.add('loading'); @post('/do_something')">
3        Do something
4        <span>Loading...</span>
5    </button>
6</div>

Optimistic Updates #

Optimistic updates (also known as optimistic UI) are when the UI updates immediately as if an operation succeeded, before the backend actually confirms it. It is a strategy used to makes web apps feel snappier, when it in fact deceives the user. Imagine seeing a confirmation message that an action succeeded, only to be shown a second later that it actually failed. Rather than deceive the user, use loading indicators to show the user that the action is in progress, and only confirm success from the backend (see this example).

Accessibility #

The web should be accessible to everyone. Datastar stays out of your way and leaves accessibility to you. Use semantic HTML, apply ARIA where it makes sense, and ensure your app works well with keyboards and screen readers. Here’s an example of using adata-attr to apply ARIA attributes to a button than toggles the visibility of a menu.

1<button data-on:click="$_menuOpen = !$_menuOpen"
2        data-attr:aria-expanded="$_menuOpen ? 'true' : 'false'"
3>
4    Open/Close Menu
5</button>
6<div data-attr:aria-hidden="$_menuOpen ? 'false' : 'true'"></div>

Reference

Attributes

Data attributes are evaluated in the order they appear in the DOM, have special casing rules, can be aliased to avoid conflicts with other libraries, can contain Datastar expressions, and have runtime error handling.

The Datastar VSCode extension and IntelliJ plugin provide autocompletion for all available data-* attributes.

data-attr #

Sets the value of any HTML attribute to an expression, and keeps it in sync.

1<div data-attr:title="$foo"></div>

The data-attr attribute can also be used to set the values of multiple attributes on an element using a set of key-value pairs, where the keys represent attribute names and the values represent expressions.

1<div data-attr="{title: $foo, disabled: $bar}"></div>

data-bind #

Creates a signal (if one doesn’t already exist) and sets up two-way data binding between it and an element’s value. This means that the value of the element is updated when the signal changes, and the signal value is updated when the value of the element changes.

The data-bind attribute can be placed on any HTML element on which data can be input or choices selected (input, select,textarea elements, and web components). Event listeners are added for change and input events.

1<input data-bind:foo />

The signal name can be specified in the key (as above), or in the value (as below). This can be useful depending on the templating language you are using.

1<input data-bind="foo" />

The initial value of the signal is set to the value of the element, unless a signal has already been defined. So in the example below, $foo is set to bar.

1<input data-bind:foo value="bar" />

Whereas in the example below, $foo inherits the value baz of the predefined signal.

1<div data-signals:foo="baz">
2    <input data-bind:foo value="bar" />
3</div>

Predefined Signal Types

When you predefine a signal, its type is preserved during binding. Whenever the element’s value changes, the signal value is automatically converted to match the original type.

For example, in the code below, $foo is set to the number 10 (not the string "10") when the option is selected.

1<div data-signals:foo="0">
2    <select data-bind:foo>
3        <option value="10">10</option>
4    </select>
5</div>

In the same way, you can assign multiple input values to a single signal by predefining it as an array. In the example below, $foo becomes ["bar", "baz"] when both checkboxes are checked, and ["", ""] when neither is checked.

1<div data-signals:foo="[]">
2    <input data-bind:foo type="checkbox" value="bar" />
3    <input data-bind:foo type="checkbox" value="baz" />
4</div>

File Uploads

Input fields of type file will automatically encode file contents in base64. This means that a form is not required.

1<input type="file" data-bind:files multiple />

The resulting signal is in the format { name: string, contents: string, mime: string }[]. See the file upload example.

If you want files to be uploaded to the server, rather than be converted to signals, use a form and with multipart/form-data in the enctype attribute. See the backend actions reference.

Modifiers

Modifiers allow you to modify behavior when binding signals using a key.

1<input data-bind:my-signal__case.kebab />

data-class #

Adds or removes a class to or from an element based on an expression.

1<div data-class:hidden="$foo"></div>

If the expression evaluates to true, the hidden class is added to the element; otherwise, it is removed.

The data-class attribute can also be used to add or remove multiple classes from an element using a set of key-value pairs, where the keys represent class names and the values represent expressions.

1<div data-class="{hidden: $foo, 'font-bold': $bar}"></div>

Modifiers

Modifiers allow you to modify behavior when defining a class name using a key.

1<div data-class:my-class__case.camel="$foo"></div>

data-computed #

Creates a signal that is computed based on an expression. The computed signal is read-only, and its value is automatically updated when any signals in the expression are updated.

1<div data-computed:foo="$bar + $baz"></div>

Computed signals are useful for memoizing expressions containing other signals. Their values can be used in other expressions.

1<div data-computed:foo="$bar + $baz"></div>
2<div data-text="$foo"></div>
Computed signal expressions must not be used for performing actions (changing other signals, actions, JavaScript functions, etc.). If you need to perform an action in response to a signal change, use the data-effect attribute.

The data-computed attribute can also be used to create computed signal using a set of key-value pairs, where the keys represent signal names and the values are callables (usually arrow functions) that return a reactive value.

1<div data-computed="{foo: () => $bar + $baz}"></div>

Modifiers

Modifiers allow you to modify behavior when defining computed signals using a key.

1<div data-computed:my-signal__case.kebab="$bar + $baz"></div>

data-effect #

Executes an expression on page load and whenever any signals in the expression change. This is useful for performing side effects, such as updating other signals, making requests to the backend, or manipulating the DOM.

1<div data-effect="$foo = $bar + $baz"></div>

data-ignore #

Datastar walks the entire DOM and applies plugins to each element it encounters. It’s possible to tell Datastar to ignore an element and its descendants by placing a data-ignore attribute on it. This can be useful for preventing naming conflicts with third-party libraries, or when you are unable to escape user input.

1<div data-ignore data-show-thirdpartylib="">
2    <div>
3        Datastar will not process this element.
4    </div>
5</div>

Modifiers

data-ignore-morph #

Similar to the data-ignore attribute, the data-ignore-morph attribute tells the PatchElements watcher to skip processing an element and its children when morphing elements.

1<div data-ignore-morph>
2    This element will not be morphed.
3</div>
To remove the data-ignore-morph attribute from an element, simply patch the element with the data-ignore-morph attribute removed.

data-indicator #

Creates a signal and sets its value to true while a fetch request is in flight, otherwise false. The signal can be used to show a loading indicator.

1<button data-on:click="@get('/endpoint')"
2        data-indicator:fetching
3></button>

This can be useful for showing a loading spinner, disabling a button, etc.

1<button data-on:click="@get('/endpoint')"
2        data-indicator:fetching
3        data-attr:disabled="$fetching"
4></button>
5<div data-show="$fetching">Loading...</div>

The signal name can be specified in the key (as above), or in the value (as below). This can be useful depending on the templating language you are using.

1<button data-indicator="fetching"></button>

When using data-indicator with a fetch request initiated in a data-init attribute, you should ensure that the indicator signal is created before the fetch request is initialized.

1<div data-indicator:fetching data-init="@get('/endpoint')"></div>

Modifiers

Modifiers allow you to modify behavior when defining indicator signals using a key.

data-init #

Runs an expression when the attribute is initialized. This can happen on page load, when an element is patched into the DOM, and any time the attribute is modified (via a backend action or otherwise).

The expression contained in the data-init attribute is executed when the element attribute is loaded into the DOM. This can happen on page load, when an element is patched into the DOM, and any time the attribute is modified (via a backend action or otherwise).
1<div data-init="$count = 1"></div>

Modifiers

Modifiers allow you to add a delay to the event listener.

1<div data-init__delay.500ms="$count = 1"></div>

data-json-signals #

Sets the text content of an element to a reactive JSON stringified version of signals. Useful when troubleshooting an issue.

1<!-- Display all signals -->
2<pre data-json-signals></pre>

You can optionally provide a filter object to include or exclude specific signals using regular expressions.

1<!-- Only show signals that include "user" in their path -->
2<pre data-json-signals="{include: /user/}"></pre>
3
4<!-- Show all signals except those ending with "temp" -->
5<pre data-json-signals="{exclude: /temp$/}"></pre>
6
7<!-- Combine include and exclude filters -->
8<pre data-json-signals="{include: /^app/, exclude: /password/}"></pre>

Modifiers

Modifiers allow you to modify the output format.

1<!-- Display filtered signals in a compact format -->
2<pre data-json-signals__terse="{include: /counter/}"></pre>

data-on #

Attaches an event listener to an element, executing an expression whenever the event is triggered.

1<button data-on:click="$foo = ''">Reset</button>

An evt variable that represents the event object is available in the expression.

1<div data-on:myevent="$foo = evt.detail"></div>

The data-on attribute works with events and custom events. The data-on:submit event listener prevents the default submission behavior of forms.

Modifiers

Modifiers allow you to modify behavior when events are triggered. Some modifiers have tags to further modify the behavior.

* Only works with built-in events.

1<button data-on:click__window__debounce.500ms.leading="$foo = ''"></button>
2<div data-on:my-event__case.camel="$foo = ''"></div>

data-on-intersect #

Runs an expression when the element intersects with the viewport.

1<div data-on-intersect="$intersected = true"></div>

Modifiers

Modifiers allow you to modify the element intersection behavior and the timing of the event listener.

1<div data-on-intersect__once__full="$fullyIntersected = true"></div>

data-on-interval #

Runs an expression at a regular interval. The interval duration defaults to one second and can be modified using the __duration modifier.

1<div data-on-interval="$count++"></div>

Modifiers

Modifiers allow you to modify the interval duration.

1<div data-on-interval__duration.500ms="$count++"></div>

data-on-signal-patch #

Runs an expression whenever any signals are patched. This is useful for tracking changes, updating computed values, or triggering side effects when data updates.

1<div data-on-signal-patch="console.log('A signal changed!')"></div>

The patch variable is available in the expression and contains the signal patch details.

1<div data-on-signal-patch="console.log('Signal patch:', patch)"></div>

You can filter which signals to watch using the data-on-signal-patch-filter attribute.

Modifiers

Modifiers allow you to modify the timing of the event listener.

1<div data-on-signal-patch__debounce.500ms="doSomething()"></div>

data-on-signal-patch-filter #

Filters which signals to watch when using the data-on-signal-patch attribute.

The data-on-signal-patch-filter attribute accepts an object with include and/or exclude properties that are regular expressions.

1<!-- Only react to counter signal changes -->
2<div data-on-signal-patch-filter="{include: /^counter$/}"></div>
3
4<!-- React to all changes except those ending with "changes" -->
5<div data-on-signal-patch-filter="{exclude: /changes$/}"></div>
6
7<!-- Combine include and exclude filters -->
8<div data-on-signal-patch-filter="{include: /user/, exclude: /password/}"></div>

data-preserve-attr #

Preserves the value of an attribute when morphing DOM elements.

1<details open data-preserve-attr="open">
2    <summary>Title</summary>
3    Content
4</details>

You can preserve multiple attributes by separating them with a space.

1<details open class="foo" data-preserve-attr="open class">
2    <summary>Title</summary>
3    Content
4</details>

data-ref #

Creates a new signal that is a reference to the element on which the data attribute is placed.

1<div data-ref:foo></div>

The signal name can be specified in the key (as above), or in the value (as below). This can be useful depending on the templating language you are using.

1<div data-ref="foo"></div>

The signal value can then be used to reference the element.

1$foo is a reference to a <span data-text="$foo.tagName"></span> element

Modifiers

Modifiers allow you to modify behavior when defining references using a key.

1<div data-ref:my-signal__case.kebab></div>

data-show #

Shows or hides an element based on whether an expression evaluates to true or false. For anything with custom requirements, use data-class instead.

1<div data-show="$foo"></div>

To prevent flickering of the element before Datastar has processed the DOM, you can add a display: none style to the element to hide it initially.

1<div data-show="$foo" style="display: none"></div>

data-signals #

Patches (adds, updates or removes) one or more signals into the existing signals. Values defined later in the DOM tree override those defined earlier.

1<div data-signals:foo="1"></div>

Signals can be nested using dot-notation.

1<div data-signals:foo.bar="1"></div>

The data-signals attribute can also be used to patch multiple signals using a set of key-value pairs, where the keys represent signal names and the values represent expressions.

1<div data-signals="{foo: {bar: 1, baz: 2}}"></div>

The value above is written in JavaScript object notation, but JSON, which is a subset and which most templating languages have built-in support for, is also allowed.

Setting a signal’s value to null or undefined removes the signal.

1<div data-signals="{foo: null}"></div>

Keys used in data-signals:* are converted to camel case, so the signal name mySignal must be written as data-signals:my-signal or data-signals="{mySignal: 1}".

Signals beginning with an underscore are not included in requests to the backend by default. You can opt to include them by modifying the value of the filterSignals option.

Signal names cannot begin with nor contain a double underscore (__), due to its use as a modifier delimiter.

Modifiers

Modifiers allow you to modify behavior when patching signals using a key.

1<div data-signals:my-signal__case.kebab="1"
2     data-signals:foo__ifmissing="1"
3></div>

data-style #

Sets the value of inline CSS styles on an element based on an expression, and keeps them in sync.

1<div data-style:background-color="$usingRed ? 'red' : 'blue'"></div>
2<div data-style:display="$hiding && 'none'"></div>

The data-style attribute can also be used to set multiple style properties on an element using a set of key-value pairs, where the keys represent CSS property names and the values represent expressions.

1<div data-style="{
2    display: $hiding ? 'none' : 'flex',
3    flexDirection: 'column',
4    color: $usingRed ? 'red' : 'green'
5}"></div>

Style properties can be specified in either camelCase (e.g., backgroundColor) or kebab-case (e.g., background-color). They will be automatically converted to the appropriate format.

Empty string, null, undefined, or false values will restore the original inline style value if one existed, or remove the style property if there was no initial value. This allows you to use the logical AND operator (&&) for conditional styles: $condition && 'value' will apply the style when the condition is true and restore the original value when false.

1<!-- When $x is false, color remains red from inline style -->
2<div style="color: red;" data-style:color="$x && 'green'"></div>
3
4<!-- When $hiding is true, display becomes none; when false, reverts to flex from inline style -->
5<div style="display: flex;" data-style:display="$hiding && 'none'"></div>

The plugin tracks initial inline style values and restores them when data-style expressions become falsy or during cleanup. This ensures existing inline styles are preserved and only the dynamic changes are managed by Datastar.

data-text #

Binds the text content of an element to an expression.

1<div data-text="$foo"></div>

Pro Attributes #

The Pro attributes add functionality to the free open source Datastar framework. These attributes are available under a commercial license that helps fund our open source work.

data-animate #Pro

Allows you to animate element attributes over time. Animated attributes are updated reactively whenever signals used in the expression change.

data-custom-validity #Pro

Allows you to add custom validity to an element using an expression. The expression must evaluate to a string that will be set as the custom validity message. If the string is empty, the input is considered valid. If the string is non-empty, the input is considered invalid and the string is used as the reported message.

1<form>
2    <input data-bind:foo name="foo" />
3    <input data-bind:bar name="bar"
4           data-custom-validity="$foo === $bar ? '' : 'Values must be the same.'"
5    />
6    <button>Submit form</button>
7</form>

data-on-raf #Pro

Runs an expression on every requestAnimationFrame event.

1<div data-on-raf="$count++"></div>

Modifiers

Modifiers allow you to modify the timing of the event listener.

1<div data-on-raf__throttle.10ms="$count++"></div>

data-on-resize #Pro

Runs an expression whenever an element’s dimensions change.

1<div data-on-resize="$count++"></div>

Modifiers

Modifiers allow you to modify the timing of the event listener.

1<div data-on-resize__debounce.10ms="$count++"></div>

data-persist #Pro

Persists signals in local storage. This is useful for storing values between page loads.

1<div data-persist></div>

The signals to be persisted can be filtered by providing a value that is an object with include and/or exclude properties that are regular expressions.

1<div data-persist="{include: /foo/, exclude: /bar/}"></div>

You can use a custom storage key by adding it after data-persist:. By default, signals are stored using the key datastar.

1<div data-persist:mykey></div>

Modifiers

Modifiers allow you to modify the storage target.

1<!-- Persists signals using a custom key `mykey` in session storage -->
2<div data-persist:mykey__session></div>

data-query-string #Pro

Syncs query string params to signal values on page load, and syncs signal values to query string params on change.

1<div data-query-string></div>

The signals to be synced can be filtered by providing a value that is an object with include and/or exclude properties that are regular expressions.

1<div data-query-string="{include: /foo/, exclude: /bar/}"></div>

Modifiers

Modifiers allow you to enable history support.

1<div data-query-string__filter__history></div>

data-replace-url #Pro

Replaces the URL in the browser without reloading the page. The value can be a relative or absolute URL, and is an evaluated expression.

1<div data-replace-url="`/page${page}`"></div>

data-scroll-into-view #Pro

Scrolls the element into view. Useful when updating the DOM from the backend, and you want to scroll to the new content.

1<div data-scroll-into-view></div>

Modifiers

Modifiers allow you to modify scrolling behavior.

data-rocket #Pro

Creates a Rocket web component. See the Rocket reference for details.

data-view-transition #Pro

Sets the view-transition-name style attribute explicitly.

1<div data-view-transition="$foo"></div>

Page level transitions are automatically handled by an injected meta tag. Inter-page elements are automatically transitioned if the View Transition API is available in the browser and useViewTransitions is true.

Attribute Order #

Elements are evaluated by walking the DOM in a depth-first manner, and attributes are processed in the order they appear in the element. This is important in some cases, such as when using data-indicator with a fetch request initiated in a data-init attribute, in which the indicator signal must be created before the fetch request is initialized.

1<div data-indicator:fetching data-init="@get('/endpoint')"></div>

Attribute Casing #

According to the HTML specification, all data-* attributes (not Datastar the framework, but any time a data attribute appears in the DOM) are case in-sensitive, but are converted to camelCase when accessed from JavaScript by Datastar.

Datastar handles casing of data attributes in two ways:

  1. The keys used in attributes that define signals (data-signals:*, data-computed:*, etc.), are converted to camelCase. For example, data-signals:my-signal defines a signal named mySignal. You would use the signal in a Datastar expression as $mySignal.
  2. The keys used by all other attributes are, by default, converted to kebab-case. For example, data-class:text-blue-700 adds or removes the class text-blue-700, and data-on:rocket-launched would react to the event named rocket-launched.

You can use the __case modifier to convert between camelCase, kebab-case, snake_case, and PascalCase, or alternatively use object syntax when available.

For example, if listening for an event called widgetLoaded, you would use data-on:widget-loaded__case.camel.

Aliasing Attributes #

It is possible to alias data-* attributes to a custom alias (data-foo-*, for example) using the bundler. A custom alias should only be used if you have a conflict with a legacy library and data-ignore cannot be used.

We maintain a data-star-* aliased version that can be included as follows.

1<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar-aliased.js"></script>

Datastar Expressions #

Datastar expressions used in data-* attributes can parse signals (which are prefixed with a $).

A variable el is available in every Datastar expression, representing the element that the attribute exists on.

1<div id="bar" data-text="$foo + el.id"></div>

Read more about Datastar expressions in the guide.

Error Handling #

Datastar has built-in error handling and reporting for runtime errors. When a data attribute is used incorrectly, for example data-text-foo, the following error message is logged to the browser console.

 1Uncaught datastar runtime error: textKeyNotAllowed
 2More info: https://data-star.dev/errors/key_not_allowed?metadata=%7B%22plugin%22%3A%7B%22name%22%3A%22text%22%2C%22type%22%3A%22attribute%22%7D%2C%22element%22%3A%7B%22id%22%3A%22%22%2C%22tag%22%3A%22DIV%22%7D%2C%22expression%22%3A%7B%22rawKey%22%3A%22textFoo%22%2C%22key%22%3A%22foo%22%2C%22value%22%3A%22%22%2C%22fnContent%22%3A%22%22%7D%7D
 3Context: {
 4    "plugin": {
 5        "name": "text",
 6        "type": "attribute"
 7    },
 8    "element": {
 9        "id": "",
10        "tag": "DIV"
11    },
12    "expression": {
13        "rawKey": "textFoo",
14        "key": "foo",
15        "value": "",
16        "fnContent": ""
17    }
18}

The “More info” link takes you directly to a context-aware error page that explains error and provides correct sample usage. See the error page for the example above, and all available error messages in the sidebar menu.

Actions

Datastar provides actions (helper functions) that can be used in Datastar expressions.

The @ prefix designates actions that are safe to use in expressions. This is a security feature that prevents arbitrary JavaScript from being executed in the browser. Datastar uses Function() constructors to create and execute these actions in a secure and controlled sandboxed environment.

@peek() #

@peek(callable: () => any)

Allows accessing signals without subscribing to their changes in expressions.

1<div data-text="$foo + @peek(() => $bar)"></div>

In the example above, the expression in the data-text attribute will be re-evaluated whenever $foo changes, but it will not be re-evaluated when $bar changes, since it is evaluated inside the @peek() action.

@setAll() #

@setAll(value: any, filter?: {include: RegExp, exclude?: RegExp})

Sets the value of all matching signals (or all signals if no filter is used) to the expression provided in the first argument. The second argument is an optional filter object with an include property that accepts a regular expression to match signal paths. You can optionally provide an exclude property to exclude specific patterns.

The Datastar Inspector can be used to inspect and filter current signals and view signal patch events in real-time.
 1<!-- Sets the `foo` signal only -->
 2<div data-signals:foo="false">
 3    <button data-on:click="@setAll(true, {include: /^foo$/})"></button>
 4</div>
 5
 6<!-- Sets all signals starting with `user.` -->
 7<div data-signals="{user: {name: '', nickname: ''}}">
 8    <button data-on:click="@setAll('johnny', {include: /^user\./})"></button>
 9</div>
10
11<!-- Sets all signals except those ending with `_temp` -->
12<div data-signals="{data: '', data_temp: '', info: '', info_temp: ''}">
13    <button data-on:click="@setAll('reset', {include: /.*/, exclude: /_temp$/})"></button>
14</div>

@toggleAll() #

@toggleAll(filter?: {include: RegExp, exclude?: RegExp})

Toggles the boolean value of all matching signals (or all signals if no filter is used). The argument is an optional filter object with an include property that accepts a regular expression to match signal paths. You can optionally provide an exclude property to exclude specific patterns.

The Datastar Inspector can be used to inspect and filter current signals and view signal patch events in real-time.
 1<!-- Toggles the `foo` signal only -->
 2<div data-signals:foo="false">
 3    <button data-on:click="@toggleAll({include: /^foo$/})"></button>
 4</div>
 5
 6<!-- Toggles all signals starting with `is` -->
 7<div data-signals="{isOpen: false, isActive: true, isEnabled: false}">
 8    <button data-on:click="@toggleAll({include: /^is/})"></button>
 9</div>
10
11<!-- Toggles signals starting with `settings.` -->
12<div data-signals="{settings: {darkMode: false, autoSave: true}}">
13    <button data-on:click="@toggleAll({include: /^settings\./})"></button>
14</div>

Backend Actions #

@get() #

@get(uri: string, options={ })

Sends a GET request to the backend using the Fetch API. The URI can be any valid endpoint and the response must contain zero or more Datastar SSE events.

1<button data-on:click="@get('/endpoint')"></button>

By default, requests are sent with a Datastar-Request: true header, and a {datastar: *} object containing all existing signals, except those beginning with an underscore. This behavior can be changed using the filterSignals option, which allows you to include or exclude specific signals using regular expressions.

When using a get request, the signals are sent as a query parameter, otherwise they are sent as a JSON body.

When a page is hidden (in a background tab, for example), the default behavior for get requests is for the SSE connection to be closed, and reopened when the page becomes visible again. To keep the connection open when the page is hidden, set the openWhenHidden option to true.

1<button data-on:click="@get('/endpoint', {openWhenHidden: true})"></button>

It’s possible to send form encoded requests by setting the contentType option to form. This sends requests using application/x-www-form-urlencoded encoding.

1<button data-on:click="@get('/endpoint', {contentType: 'form'})"></button>

It’s also possible to send requests using multipart/form-data encoding by specifying it in the form element’s enctype attribute. This should be used when uploading files. See the form data example.

1<form enctype="multipart/form-data">
2    <input type="file" name="file" />
3    <button data-on:click="@get('/endpoint', {contentType: 'form'})"></button>
4</form>

@post() #

@post(uri: string, options={ })

Works the same as @get() but sends a POST request to the backend.

1<button data-on:click="@post('/endpoint')"></button>

@put() #

@put(uri: string, options={ })

Works the same as @get() but sends a PUT request to the backend.

1<button data-on:click="@put('/endpoint')"></button>

@patch() #

@patch(uri: string, options={ })

Works the same as @get() but sends a PATCH request to the backend.

1<button data-on:click="@patch('/endpoint')"></button>

@delete() #

@delete(uri: string, options={ })

Works the same as @get() but sends a DELETE request to the backend.

1<button data-on:click="@delete('/endpoint')"></button>

Options #

All of the actions above take a second argument of options.

1<button data-on:click="@get('/endpoint', {
2    filterSignals: {include: /^foo\./},
3    headers: {
4        'X-Csrf-Token': 'JImikTbsoCYQ9oGOcvugov0Awc5LbqFsZW6ObRCxuq',
5    },
6    openWhenHidden: true,
7    requestCancellation: 'disabled',
8})"></button>

Request Cancellation #

By default, when a new fetch request is initiated on an element, any existing request on that same element is automatically cancelled. This prevents multiple concurrent requests from conflicting with each other and ensures clean state management.

For example, if a user rapidly clicks a button that triggers a backend action, only the most recent request will be processed:

1<!-- Clicking this button multiple times will cancel previous requests (default behavior) -->
2<button data-on:click="@get('/slow-endpoint')">Load Data</button>

This automatic cancellation happens at the element level, meaning requests on different elements can run concurrently without interfering with each other.

You can control this behavior using the requestCancellation option:

1<!-- Allow concurrent requests (no automatic cancellation) -->
2<button data-on:click="@get('/endpoint', {requestCancellation: 'disabled'})">Allow Multiple</button>
3
4<!-- Custom abort controller for fine-grained control -->
5<div data-signals:controller="new AbortController()">
6    <button data-on:click="@get('/endpoint', {requestCancellation: $controller})">Start Request</button>
7    <button data-on:click="$controller.abort()">Cancel Request</button>
8</div>

Response Handling #

Backend actions automatically handle different response content types:

text/html

When returning HTML (text/html), the server can optionally include the following response headers:

1response.headers.set('Content-Type', 'text/html')
2response.headers.set('datastar-selector', '#my-element')
3response.headers.set('datastar-mode', 'inner')
4response.body = '<p>New content</p>'

application/json

When returning JSON (application/json), the server can optionally include the following response header:

1response.headers.set('Content-Type', 'application/json')
2response.headers.set('datastar-only-if-missing', 'true')
3response.body = JSON.stringify({ foo: 'bar' })

text/javascript

When returning JavaScript (text/javascript), the server can optionally include the following response header:

1response.headers.set('Content-Type', 'text/javascript')
2response.headers.set('datastar-script-attributes', JSON.stringify({ type: 'module' }))
3response.body = 'console.log("Hello from server!");'

Events #

All of the actions above trigger datastar-fetch events during the fetch request lifecycle. The event type determines the stage of the request.

1<div data-on:datastar-fetch="
2    evt.detail.type === 'error' && console.log('Fetch error encountered')
3"></div>

Pro Actions #

@clipboard() #Pro

@clipboard(text: string, isBase64?: boolean)

Copies the provided text to the clipboard. If the second parameter is true, the text is treated as Base64 encoded, and is decoded before copying.

Base64 encoding is useful when copying content that contains special characters, quotes, or code fragments that might not be valid within HTML attributes. This prevents parsing errors and ensures the content is safely embedded in data-* attributes.
1<!-- Copy plain text -->
2<button data-on:click="@clipboard('Hello, world!')"></button>
3
4<!-- Copy base64 encoded text (will decode before copying) -->
5<button data-on:click="@clipboard('SGVsbG8sIHdvcmxkIQ==', true)"></button>

@fit() #Pro

@fit(v: number, oldMin: number, oldMax: number, newMin: number, newMax: number, shouldClamp=false, shouldRound=false)

Linearly interpolates a value from one range to another. This is useful for converting between different scales, such as mapping a slider value to a percentage or converting temperature units.

The optional shouldClamp parameter ensures the result stays within the new range, and shouldRound rounds the result to the nearest integer.

 1<!-- Convert a 0-100 slider to 0-255 RGB value -->
 2<div>
 3    <input type="range" min="0" max="100" value="50" data-bind:slider-value>
 4    <div data-computed:rgb-value="@fit($sliderValue, 0, 100, 0, 255)">
 5        RGB Value: <span data-text="$rgbValue"></span>
 6    </div>
 7</div>
 8
 9<!-- Convert Celsius to Fahrenheit -->
10<div>
11    <input type="number" data-bind:celsius value="20" />
12    <div data-computed:fahrenheit="@fit($celsius, 0, 100, 32, 212)">
13        <span data-text="$celsius"></span>°C = <span data-text="$fahrenheit.toFixed(1)"></span>°F
14    </div>
15</div>
16
17<!-- Map mouse position to element opacity (clamped) -->
18<div
19    data-signals:mouse-x="0"
20    data-computed:opacity="@fit($mouseX, 0, window.innerWidth, 0, 1, true)"
21    data-on:mousemove__window="$mouseX = evt.clientX"
22    data-attr:style="'opacity: ' + $opacity"
23>
24    Move your mouse horizontally to change opacity
25</div>

Rocket

Rocket is currently in alpha – available with Datastar Pro.

Rocket is a Datastar Pro plugin that bridges Web Components with Datastar’s reactive system. It allows you to create encapsulated, reusable components with reactive data binding.

Rocket is a powerful feature, and should be used sparingly. For most applications, standard Datastar templates and global signals are sufficient. Reserve Rocket for cases where component encapsulation is essential, such as integrating third-party libraries or creating complex, reusable UI elements.

Basic example #

Traditional web components require verbose class definitions and manual DOM management. Rocket eliminates this complexity with a declarative, template-based approach.

Here’s a Rocket component compared to a vanilla web component.

 1<template data-rocket:simple-counter
 2          data-props:count="int|min:0|=0"
 3          data-props:start="int|min:0|=0"
 4          data-props:step="int|min:1|max:10|=1"
 5>
 6  <script>
 7    $$count = $$start
 8  </script>
 9  <template data-if="$$errs?.start">
10    <div data-text="$$errs.start[0].value"></div>
11  </template>
12  <template data-if="$$errs?.step">
13    <div data-text="$$errs.step[0].value"></div>
14  </template>
15  <button data-on:click="$$count -= $$step">-</button>
16  <span data-text="$$count"></span>
17  <button data-on:click="$$count += $$step">+</button>
18  <button data-on:click="$$count = $$start">Reset</button>
19</template>
  1class SimpleCounter extends HTMLElement {
  2  static observedAttributes = ['start', 'step'];
  3  
  4  constructor() {
  5    super();
  6    this.innerHTML = `
  7      <div class="error" style="display: none;"></div>
  8      <button class="dec">-</button>
  9      <span class="count">0</span>
 10      <button class="inc">+</button>
 11      <button class="reset">Reset</button>
 12    `;
 13    
 14    this.errorEl = this.querySelector('.error');
 15    this.decBtn = this.querySelector('.dec');
 16    this.incBtn = this.querySelector('.inc');
 17    this.resetBtn = this.querySelector('.reset');
 18    this.countEl = this.querySelector('.count');
 19    
 20    this.handleDec = () => { 
 21      const newValue = this.count - this.step;
 22      if (newValue >= 0) {
 23        this.count = newValue;
 24        this.updateDisplay();
 25      }
 26    };
 27    this.handleInc = () => { 
 28      this.count += this.step;
 29      this.updateDisplay();
 30    };
 31    this.handleReset = () => { 
 32      this.count = this.start; 
 33      this.updateDisplay(); 
 34    };
 35    
 36    this.decBtn.addEventListener('click', this.handleDec);
 37    this.incBtn.addEventListener('click', this.handleInc);
 38    this.resetBtn.addEventListener('click', this.handleReset);
 39  }
 40  
 41  connectedCallback() {
 42    const startVal = parseInt(this.getAttribute('start') || '0');
 43    const stepVal = parseInt(this.getAttribute('step') || '1');
 44    
 45    if (startVal < 0) {
 46      this.errorEl.textContent = 'start must be at least 0';
 47      this.errorEl.style.display = 'block';
 48      this.start = 0;
 49    } else {
 50      this.start = startVal;
 51      this.errorEl.style.display = 'none';
 52    }
 53    
 54    if (stepVal < 1 || stepVal > 10) {
 55      this.errorEl.textContent = 'step must be between 1 and 10';
 56      this.errorEl.style.display = 'block';
 57      this.step = Math.max(1, Math.min(10, stepVal));
 58    } else {
 59      this.step = stepVal;
 60      if (this.start === startVal) {
 61        this.errorEl.style.display = 'none';
 62      }
 63    }
 64    
 65    this.count = this.start;
 66    this.updateDisplay();
 67  }
 68  
 69  disconnectedCallback() {
 70    this.decBtn.removeEventListener('click', this.handleDec);
 71    this.incBtn.removeEventListener('click', this.handleInc);
 72    this.resetBtn.removeEventListener('click', this.handleReset);
 73  }
 74  
 75  attributeChangedCallback(name, oldValue, newValue) {
 76    if (name === 'start') {
 77      const startVal = parseInt(newValue || '0');
 78      if (startVal < 0) {
 79        this.errorEl.textContent = 'start must be at least 0';
 80        this.errorEl.style.display = 'block';
 81        this.start = 0;
 82      } else {
 83        this.start = startVal;
 84        this.errorEl.style.display = 'none';
 85      }
 86      this.count = this.start;
 87    } else if (name === 'step') {
 88      const stepVal = parseInt(newValue || '1');
 89      if (stepVal < 1 || stepVal > 10) {
 90        this.errorEl.textContent = 'step must be between 1 and 10';
 91        this.errorEl.style.display = 'block';
 92        this.step = Math.max(1, Math.min(10, stepVal));
 93      } else {
 94        this.step = stepVal;
 95        this.errorEl.style.display = 'none';
 96      }
 97    }
 98    if (this.isConnected) {
 99      this.updateDisplay();
100    }
101  }
102  
103  updateDisplay() {
104    this.countEl.textContent = this.count;
105  }
106}
107
108customElements.define('simple-counter', SimpleCounter);

Overview #

Rocket allows you to turn HTML templates into fully reactive web components. The backend remains the source of truth, but your frontend components are now encapsulated and reusable without any of the usual hassle.

Add data-rocket:my-component to a template element to turn it into a Rocket component. Component signals are automatically scoped with $$, so component instances don’t interfere with each other.

You can use Rocket to wrap external libraries using module imports, and create references to elements within your component. Each component gets its own signal namespace that plays nicely with Datastar’s global signals. When you remove a component from the DOM, all its $$ signals are cleaned up automatically.

Bridging Web Components and Datastar #

Web components want encapsulation; Datastar wants a global signal store. Rocket gives you both by creating isolated namespaces for each component. Each instance gets its own sandbox that doesn’t mess with other components on the page, or with global signals.

Multiple component instances work seamlessly, each getting its own numbered namespace. You still have access to global signals when you need them, but your component state stays isolated and clean.

Signal Scoping #

Use $$ for component-scoped signals, and $ for global signals. Component signals are automatically cleaned up when you remove the component from the DOM - no memory leaks, no manual cleanup required.

Behind the scenes, your $$count becomes something like $._rocket.my_counter._1.count, with each instance getting its own numbered namespace. You never have to think about this complexity - just write $$count and Rocket handles the rest.

 1// Your component template writes:
 2<button data-on:click="$$count++">Increment</button>
 3<span data-text="$$count"></span>
 4
 5// Rocket transforms it to (for instance #1):
 6<button data-on:click="$._rocket.my_counter._1.count++">Increment</button>
 7<span data-text="$._rocket.my_counter._1.count"></span>
 8
 9// The global Datastar signal structure:
10$._rocket = {
11  my_counter: {
12    _1: { count: 0 }, // First counter instance
13    _2: { count: 5 }, // Second counter instance
14    _3: { count: 10 } // Third counter instance
15  },
16  user_card: {
17    _4: { name: "Alice" }, // Different component type
18    _5: { name: "Bob" }
19  }
20}

Defining Rocket Components #

Rocket components are defined using a HTML template element with the data-rocket:my-component attribute, where my-component is the name of the resulting web component. The name must contain at least one hyphen, as per the custom element specification.

1<template data-rocket:my-counter>
2  <script>
3    $$count = 0  
4  </script>
5  <button data-on:click="$$count++">
6    Count: <span data-text="$$count"></span>
7  </button>
8</template>

This gets compiled to a web component, meaning that usage is simply:

1<my-counter></my-counter>

Rocket components must be defined before being used in the DOM.

1<!-- Template element must appear first in the DOM. -->
2<template data-rocket:my-counter></template>
3
4<my-counter></my-counter>

Signal Management #

Rocket makes it possible to work with both component-scoped and global signals (global to the entire page).

Component Signals #

Component-scoped signals use the $$ prefix and are isolated to each component instance.

 1<template data-rocket:isolated-counter>
 2  <script>
 3    // These are component-scoped – each instance has its own values
 4    $$count = 0
 5    $$step = 1
 6    $$maxCount = 10
 7    $$isAtMax = computed(() => $$count >= $$maxCount)
 8    
 9    // Component actions
10    actions.increment = () => {
11      if ($$count < $$maxCount) {
12        $$count += $$step
13      }
14    }
15  </script>
16  
17  <div>
18    <p>Count: <span data-text="$$count"></span></p>
19    <p data-show="$$isAtMax" class="error">Maximum reached!</p>
20    <button data-on:click="@increment()" data-attr:disabled="$$isAtMax">+</button>
21  </div>
22</template>
23
24<!-- Multiple instances work independently -->
25<isolated-counter></isolated-counter>
26<isolated-counter></isolated-counter>

Global Signals #

Global signals use the $ prefix and are shared across the entire page.

 1<template data-rocket:theme-toggle>
 2  <script>
 3    // Access global theme state
 4    if (!$theme) {
 5      $theme = 'light'
 6    }
 7    
 8    actions.toggleTheme = () => {
 9      $theme = $theme === 'light' ? 'dark' : 'light'
10    }
11  </script>
12  
13  <button data-on:click="@toggleTheme()">
14    <span data-text="$theme === 'light' ? '🌙' : '☀️'"></span>
15    <span data-text="$theme === 'light' ? 'Dark Mode' : 'Light Mode'"></span>
16  </button>
17</template>
18
19<!-- All instances share the same global theme -->
20<theme-toggle></theme-toggle>
21<theme-toggle></theme-toggle>

Props #

The data-props:* attribute allows you to define component props with codecs for validation and defaults.

 1<!-- Component definition with defaults -->
 2<template data-rocket:progress-bar
 3          data-props:value="int|=0"
 4          data-props:max="int|=100" 
 5          data-props:color="string|=blue"
 6>
 7  <script>
 8    $$percentage = computed(() => Math.round(($$value / $$max) * 100))
 9  </script>
10  
11  <div class="progress-container">
12    <div class="progress-bar" 
13        data-style="{
14          width: $$percentage + '%',
15          backgroundColor: $$color
16        }">
17    </div>
18    <span data-text="$$percentage + '%'"></span>
19  </div>
20</template>
21
22<!-- Usage -->
23<progress-bar data-attr:value="'75'" data-attr:color="'green'"></progress-bar>
24<progress-bar data-attr:value="'30'" data-attr:max="'50'"></progress-bar>

Rocket automatically transforms and validates values using the codecs defined in data-props:* attributes.

Setup Scripts #

Setup scripts initialize component behavior and run when the component is created. Rocket supports both component (per-instance) and static (one-time) setup scripts.

Component Setup Scripts #

Regular <script> tags run for each component instance.

 1<template data-rocket:timer
 2          data-props:seconds="int|=0"
 3          data-props:running="boolean|=false"
 4          data-props:interval="int|=1000"
 5>
 6  <script>
 7    $$minutes = computed(() => Math.floor($$seconds / 60))
 8    $$displayTime = computed(() => {
 9      const m = String($$minutes).padStart(2, '0')
10      const s = String($$seconds % 60).padStart(2, '0')
11      return m + ':' + s
12    })
13    
14    let intervalId
15    effect(() => {
16      if ($$running) {
17        intervalId = setInterval(() => $$seconds++, $$interval)
18      } else {
19        clearInterval(intervalId)
20      }
21    })
22    
23    // Cleanup when component is removed
24    onCleanup(() => {
25      clearInterval(intervalId)
26    })
27  </script>
28  
29  <div>
30    <h2 data-text="$$displayTime"></h2>
31    <button data-on:click="$$running = !$$running" 
32            data-text="$$running ? 'Stop' : 'Start'">
33    </button>
34    <button data-on:click="$$seconds = 0">Reset</button>
35</div>
36</template>

Host Element Access #

Rocket injects an el binding into every component setup script. It always points to the current custom element instance, even when you opt into Shadow DOM, so you can imperatively read attributes, toggle classes, or wire event listeners.

1<template data-rocket:focus-pill>
2  <script>
3    el.setAttribute('role', 'button')
4    el.addEventListener('focus', () => el.classList.add('is-focused'))
5    el.addEventListener('blur', () => el.classList.remove('is-focused'))
6  </script>
7  
8  <span><slot></slot></span>
9</template>

Setup code executes inside an arrow function sandbox, so this has no meaning inside component scripts. Use el any time you need the host element—for example to call el.shadowRoot, el.setAttribute, or pass it into a third-party library.

Static Setup Scripts #

Scripts with a data-static attribute only run once, when the component type is first registered. This is useful for shared constants or utilities.

 1<template data-rocket:icon-button>
 2  <script data-static>
 3    const icons = {
 4      heart: '❤️',
 5      star: '⭐',
 6      thumbs: '👍',
 7      fire: '🔥'
 8    }
 9  </script>
10  
11  <script>
12    $$icon = $$type || 'heart'
13    $$emoji = computed(() => icons[$$icon] || '❓')
14  </script>
15  
16  <button data-on:click="@click()">
17    <span data-text="$$emoji"></span>
18    <span data-text="$$label || 'Click me'"></span>
19  </button>
20</template>

Module Imports #

Rocket allows you to wrap external libraries, loading them before the component initializes and the setup script runs. Use data-import-esm:* for modern ES modules and data-import-iife:* for legacy global libraries.

ESM Imports #

The data-import-esm:* attribute should be used for modern ES modules.

 1<template data-rocket:qr-generator
 2          data-props:text="string|trim|required!|=Hello World"
 3          data-props:size="int|min:50|max:1000|=200"
 4          data-import-esm:qr="https://cdn.jsdelivr.net/npm/[email protected]/+esm"
 5>
 6  <script>
 7    $$errorText = ''
 8    
 9    effect(() => {
10      // Check for validation errors first
11      if ($$hasErrs) {
12        const messages = []
13        if ($$errs?.text) {
14          messages.push('Text is required')
15        }
16        if ($$errs?.size) {
17          messages.push('Size must be 50-1000px')
18        }
19        $$errorText = messages.join(', ') || 'Validation failed'
20        return
21      }
22
23      if (!$$canvas) {
24        return
25      }
26
27      if (!qr) {
28        $$errorText = 'QR library not loaded'
29        return
30      }
31      
32      try {
33        qr.render({
34          text: $$text,
35          size: $$size
36        }, $$canvas)
37        $$errorText = ''
38      } catch (err) {
39        $$errorText = 'QR generation failed'
40      }
41    })
42  </script>
43  
44  <div data-style="{width: $$size + 'px', height: $$size + 'px'}">
45    <template data-if="!$$errorText">
46      <canvas data-ref="canvas" style="display: block;"></canvas>
47    </template>
48    <template data-else>
49      <div data-text="$$errorText" class="error"></div>
50    </template>
51  </div>
52</template>

IIFE Imports #

The data-import-iife:* attribute should be used for legacy libraries. The library must expose a global variable that matches the alias you specify after data-import-iife:.

 1<template data-rocket:chart
 2          data-props:data="json|=[]"
 3          data-props:type="string|=line"
 4          data-import-iife:chart="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.js"
 5>
 6  <script>
 7    let chartInstance
 8    
 9    effect(() => {
10      if (!$$canvas || !chart || !$$data.length) {
11        return
12      }
13
14      if (chartInstance) {
15        chartInstance.destroy()
16      }
17      
18      const ctx = $$canvas.getContext('2d')
19      chartInstance = new chart.Chart(ctx, {
20        type: $$type,
21        data: {
22          datasets: [{
23            data: $$data,
24            backgroundColor: '#3b82f6'
25          }]
26        }
27      })
28    })
29    
30    onCleanup(() => {
31      if (chartInstance) {
32        chartInstance.destroy()
33      }
34    })
35  </script>
36  
37  <canvas data-ref="canvas"></canvas>
38</template>

Rocket Attributes #

In addition to the Rocket-specific data-* attributes defined above, the following attributes are available within Rocket components.

By default, Rocket renders into the light DOM of the custom element, so the component’s content participates directly in the page layout and inherits global styles. The shadow attributes data-shadow-* let's you opt a component into using a Shadow DOM host instead. If you’re not familiar with Shadow DOM concepts like the shadow root, it’s worth reading the MDN documentation first.

data-shadow-open #

Use data-shadow-open to force an open Shadow DOM when you want style encapsulation but still need access to internal elements via element.shadowRoot, which is useful during debugging or integration.

 1<template data-rocket:tag-pill
 2          data-shadow-open
 3          data-props:label="string|trim|required!">
 4  <style>
 5    .pill {
 6      display: inline-flex;
 7      align-items: center;
 8      padding: 0.25rem 0.5rem;
 9      border-radius: 999px;
10      background: #0f172a;
11      color: white;
12      font-size: 0.75rem;
13      gap: 0.25rem;
14    }
15    .dot {
16      width: 6px;
17      height: 6px;
18      border-radius: 999px;
19      background: #22c55e;
20    }
21  </style>
22  <div class="pill">
23    <span class="dot"></span>
24    <span data-text="$$label"></span>
25  </div>
26</template>
27
28<!-- Styles are fully encapsulated, but devtools and test harnesses can still inspect the .pill element via element.shadowRoot -->
29<tag-pill data-attr:label="'Shadow-ready'"></tag-pill>

data-shadow-closed #

Use data-shadow-closed to force a closed Shadow DOM. Choose this when you want the implementation to be fully encapsulated and inaccessible via element.shadowRoot, while still benefitting from Shadow DOM styling and slot projection.

 1<template data-rocket:status-tooltip
 2          data-shadow-closed
 3          data-props:text="string|trim|required!">
 4  <script>
 5    $$show = false
 6  </script>
 7
 8  <span data-on:mouseenter="$$show = true"
 9        data-on:mouseleave="$$show = false">
10    <slot></slot>
11    <span data-show="$$show" class="tooltip"
12          data-text="$$text"></span>
13  </span>
14</template>
15
16<!-- The tooltip DOM is hidden inside a closed shadow root -->
17<status-tooltip data-attr:text="'Hello from Rocket'">
18  Hover me
19</status-tooltip>

data-if #

Conditionally outputs an element based on an expression. Must be placed on a <template> element in Rocket components.

1<template data-if="$$items.count">
2  <div data-text="$$items.count + ' items'"></div>
3</template>

data-else-if #

Conditionally outputs an element based on an expression, if the preceding data-if condition is falsy. Must be on a <template>.

1<template data-if="$$items.count">
2  <div data-text="$$items.count + ' items found.'"></div>
3</template>
4<template data-else-if="$$items.count == 1">
5  <div data-text="$$items.count + ' item found.'"></div>
6</template>

data-else #

Outputs an element if the preceding data-if and data-else-if conditions are falsy. Must be on a <template>.

1<template data-if="$$items.count">
2  <div data-text="$$items.count + ' items found.'"></div>
3</template>
4<template data-else>
5  <div>No items found.</div>
6</template>

data-for #

Loops over any iterable (arrays, maps, sets, strings, and plain objects), and outputs the element for each item. Must be placed on a <template>.

1<template data-for="item, index in $$items">
2  <div>
3    <span data-text="index + ': ' + item.name"></span>
4  </div>
5</template>

data-key #

Provides a stable key for each iteration when used alongside data-for. Keys enable DOM reuse (Solid-like keyed loops) and must live on the same <template data-for>.

1<template data-for="item in $$items" data-key="item.id">
2  <div data-text="item.label"></div>
3</template>

The first alias (item above) is available to descendants just like any other binding. An optional second alias (index above) exposes the current key or numeric index. Nested loops are supported, and inner loop variables automatically shadow outer ones, so you can reuse names without conflicts.

1<template data-for="items in $$itemSet">
2  <div>
3    <template data-for="item in items">
4      <div>
5        <span data-text="item.name"></span>
6      </div>
7    </template>
8  </div>
9</template>

Reactive Patterns #

Rocket provides computed and effect functions for declarative reactivity. These keep your component state automatically in sync with the DOM.

Computed Values #

Computed values automatically update when their dependencies change.

 1<template data-rocket:shopping-cart
 2          data-props:items="json|=[]"
 3>
 4  <script>
 5    // Computed values automatically recalculate
 6    $$total = computed(() => 
 7      $$items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
 8    )
 9    
10    $$itemCount = computed(() =>
11      $$items.reduce((sum, item) => sum + item.quantity, 0)
12    )
13    
14    $$isEmpty = computed(() => $$items.length === 0)
15    
16    // Actions that modify reactive state
17    actions.addItem = (item) => {
18      $$items = [...$$items, { ...item, quantity: 1 }]
19    }
20    
21    actions.removeItem = (index) => {
22      $$items = $$items.filter((_, i) => i !== index)
23    }
24  </script>
25  
26  <div>
27    <h3>Shopping Cart</h3>
28    <p data-show="$$isEmpty">Cart is empty</p>
29    <p data-show="!$$isEmpty">
30      Items: <span data-text="$$itemCount"></span> | 
31      Total: $<span data-text="$$total.toFixed(2)"></span>
32    </p>
33    
34    <template data-for="item, index in $$items">
35      <div>
36        <span data-text="item.name"></span> - 
37        <span data-text="'$' + item.price"></span>
38        <button data-on:click="@removeItem(index)">Remove</button>
39      </div>
40    </template>
41  </div>
42</template>

Effects and Watchers #

Effects run side effects when reactive values change.

 1<template data-rocket:auto-saver
 2          data-props:data="string|="
 3          data-props:last-saved="string|="
 4          data-props:saving="boolean|=false"
 5>
 6  <script>
 7    let saveTimeout
 8    
 9    // Auto-save effect
10    effect(() => {
11      if (!$$data) {
12        return
13      }
14      
15      clearTimeout(saveTimeout)
16      saveTimeout = setTimeout(async () => {
17        $$saving = true
18        try {
19          await actions.post('/api/save', { data: $$data })
20          $$lastSaved = new Date().toLocaleTimeString()
21        } catch (error) {
22          console.error('Save failed:', error)
23        } finally {
24          $$saving = false
25        }
26      }, 1000) // Debounce by 1 second
27    })
28    
29    // Theme effect
30    effect(() => {
31      if ($theme) {
32        document.body.className = $theme + '-theme'
33      }
34    })
35    
36    onCleanup(() => {
37      clearTimeout(saveTimeout)
38    })
39  </script>
40  
41  <div>
42    <textarea data-bind="data" placeholder="Start typing..."></textarea>
43    <p data-show="$$saving">Saving...</p>
44    <p data-show="$$lastSaved">Last saved: <span data-text="$$lastSaved"></span></p>
45  </div>
46</template>

Element References #

You can use data-ref to create references to elements within your component. Element references are available as $$elementName signals and automatically updated when the DOM changes.

 1<template data-rocket:canvas-painter
 2          data-props:color="string|=#000000"
 3          data-props:brush-size="int|=5"
 4>
 5  <script>
 6    let ctx
 7    let isDrawing = false
 8    
 9    // Get canvas context when canvas is available
10    effect(() => {
11      if ($$canvas) {
12        ctx = $$canvas.getContext('2d')
13        ctx.strokeStyle = $$color
14        ctx.lineWidth = $$brushSize
15        ctx.lineCap = 'round'
16      }
17    })
18    
19    // Update drawing properties
20    effect(() => {
21      if (ctx) {
22        ctx.strokeStyle = $$color
23        ctx.lineWidth = $$brushSize
24      }
25    })
26    
27    actions.startDrawing = (e) => {
28      isDrawing = true
29      const rect = $$canvas.getBoundingClientRect()
30      ctx.beginPath()
31      ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top)
32    }
33    
34    actions.draw = (e) => {
35      if (!isDrawing) {
36        return
37      }
38
39      const rect = $$canvas.getBoundingClientRect()
40      ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top)
41      ctx.stroke()
42    }
43    
44    actions.stopDrawing = () => {
45      isDrawing = false
46    }
47    
48    actions.clear = () => {
49      if (ctx) {
50        ctx.clearRect(0, 0, $$canvas.width, $$canvas.height)
51      }
52    }
53  </script>
54  
55  <div>
56    <div>
57      <label>Color: <input type="color" data-bind="color"></label>
58      <label>Size: <input type="range" min="1" max="20" data-bind="brushSize"></label>
59      <button data-on:click="@clear()">Clear</button>
60    </div>
61    
62    <canvas 
63      data-ref="canvas" 
64      width="400" 
65      height="300"
66      style="border: 1px solid #ccc"
67      data-on:mousedown="@startDrawing"
68      data-on:mousemove="@draw"
69      data-on:mouseup="@stopDrawing"
70      data-on:mouseleave="@stopDrawing">
71    </canvas>
72  </div>
73</template>

Validation with Codecs #

Rocket’s built-in codec system makes it possible to validate user input. By defining validation rules directly in your data-props:* attributes, data is automatically transformed and validated as it flows through your component.

Type Codecs #

Type codecs convert and validate prop values.

 1<template data-rocket:validated-form
 2          data-props:email="string|trim|required!|="
 3          data-props:age="int|min:18|max:120|=0"
 4          data-props:score="int|clamp:0,100|=0"
 5>
 6  <script>
 7    // Signals are automatically validated by the codec system
 8    // No need for manual codec setup - just use the signals directly
 9    
10    // Check for validation errors using the built-in $$hasErrs signal
11    // No need to create computed - $$hasErrs is automatically available
12  </script>
13  
14  <form>
15    <div>
16      <label>Email (required):</label>
17      <input type="email" data-bind="email">
18      <span data-show="$$errs?.email" class="error">Email is required</span>
19    </div>
20    
21    <div>
22      <label>Age (18-120):</label>
23      <input type="number" data-bind="age">
24      <span data-show="$$errs?.age" class="error">Age must be 18-120</span>
25    </div>
26    
27    <div>
28      <label>Score (0-100, auto-clamped):</label>
29      <input type="number" data-bind="score">
30      <span>Current: <span data-text="$$score"></span></span>
31    </div>
32    
33    <button type="submit" data-attr:disabled="$$hasErrors">
34      Submit
35    </button>
36  </form>
37</template>

For date props, omitting an explicit default will use the current time. This is evaluated when the codec runs, producing a fresh Date instance based on the current time.

1<template data-rocket:last-updated
2          data-props:serverUpdateTime="date"
3>
4            <script>
5    $$formatted = computed(() => $$serverUpdateTime.toLocaleString())
6        </script>
7  
8        <span data-text="$$formatted"></span>
9</template>

Validation Rules #

Codecs can either transform values (modify them) or validate them (check them without modifying). Use the ! suffix to make any codec validation-only.

CodecTransformValidation
Type Conversion
stringConverts to stringIs string?
intConverts to integerIs integer?
floatConverts to numberIs numeric?
dateConverts ISO strings or timestamps to a Date object (defaults to the current time)Is valid date?
booleanConverts to boolean. A missing attribute decodes to false by default, while a present-but-empty attribute (e.g. <foo-bar baz> on a baz prop) decodes to true.Is boolean?
jsonParses JSON stringValid JSON?
jsParses JS object literal
⚠️ Avoid client values
Valid JS syntax?
binaryDecodes base64Valid base64?
Validation
required-Not empty?
oneOf:a,b,cDefaults to first option if invalidIs valid option?
Numeric Constraints
min:nClamp to minimum value>= minimum?
max:nClamp to maximum value<= maximum?
clamp:min,maxClamp between min and maxIn range?
round / round:nRound to n decimal placesIs rounded?
ceil:n / floor:nCeiling/floor to n decimal placesIs ceiling/floor?
String Transforms
trimRemove leading/trailing whitespace-
upper / lowerConvert to upper/lowercase-
kebab / camelConvert case styleCorrect case?
snake / pascalConvert case styleCorrect case?
title / title:firstTitle case (all words or first only)-
String Constraints
minLength:n-Length >= n?
maxLength:nTruncates if too longLength <= n?
length:n-Length equals n?
regex:pattern-Matches regex?
startsWith:textAdds prefix if missingStarts with text?
endsWith:textAdds suffix if missingEnds with text?
includes:text-Contains text?
Advanced Numeric
lerp:min,maxLinear interpolation (0-1 to min-max)-
fit:in1,in2,out1,out2Map value from one range to another-

Component Lifecycle #

Rocket components have a simple lifecycle with automatic cleanup.

 1<template data-rocket:lifecycle-demo>
 2  <script>
 3    console.log('Component initializing...')
 4    
 5    $$mounted = true
 6    
 7    // Setup effects and timers
 8    const intervalId = setInterval(() => {
 9      console.log('Component is alive')
10    }, 5000)
11    
12    // Cleanup when component is removed from DOM
13    onCleanup(() => {
14      console.log('Component cleanup')
15      clearInterval(intervalId)
16      $$mounted = false
17    })
18  </script>
19  
20  <div>
21    <p data-show="$$mounted">Component is mounted</p>
22  </div>
23</template>

The lifecycle is as follows:

  1. Rocket processes your template and registers the component.
  2. When you add it to the DOM, the instance is created and setup scripts run to initialize your signals.
  3. The component becomes reactive and responds to data changes.
  4. When you remove it from the DOM, all onCleanup callbacks run automatically.

Optimistic UI #

Rocket pairs seamlessly with Datastar’s server-driven model to provide instant visual feedback without shifting ownership of state to the browser. In the Rocket flow example, dragging a node instantly renders its optimistic position in the SVG while the original light-DOM host remains hidden. The component adds an .is-pending class to dim the node and connected edges, signaling that the drag is provisional. Once the backend confirms the new coordinates and updates the layout, the component automatically clears the pending style.

A dedicated prop such as server-update-time="date" makes this straightforward: each tab receives an updated timestamp from the server (via SSE or a patch), Rocket decodes it into a Date (defaulting to the current time when no value is provided), and internal effects react to reconcile every view. Unlike client-owned graph editors (e.g. React Flow), the server stays the single source of truth, while the optimistic UI remains a thin layer inside the component.

Examples #

Check out the Copy Button as a basic example, the QR Code generator with validation, the ECharts integration for data visualization, the interactive 3D Globe with markers, and the Virtual Scroll example for handling large datasets efficiently.

SSE Events

Responses to backend actions with a content type of text/event-stream can contain zero or more Datastar SSE events.

The backend SDKs can handle the formatting of SSE events for you, or you can format them yourself.

Event Types #

datastar-patch-elements #

Patches one or more elements in the DOM. By default, Datastar morphs elements by matching top-level elements based on their ID.

1event: datastar-patch-elements
2data: elements <div id="foo">Hello world!</div>
3

In the example above, the element <div id="foo">Hello world!</div> will be morphed into the target element with ID foo.

Be sure to place IDs on top-level elements to be morphed, as well as on elements within them that you’d like to preserve state on (event listeners, CSS transitions, etc.).

SVG morphing in Datastar requires special handling due to XML namespaces. See the SVG morphing example.

Additional data lines can be added to the response to override the default behavior.

KeyDescription
data: mode outerMorphs the outer HTML of the elements. This is the default (and recommended) mode.
data: mode innerMorphs the inner HTML of the elements.
data: mode replaceReplaces the outer HTML of the elements.
data: mode prependPrepends the elements to the target’s children.
data: mode appendAppends the elements to the target’s children.
data: mode beforeInserts the elements before the target as siblings.
data: mode afterInserts the elements after the target as siblings.
data: mode removeRemoves the target elements from DOM.
data: selector #fooSelects the target element of the patch using a CSS selector. Not required when using the outer or replace modes.
data: useViewTransition trueWhether to use view transitions when patching elements. Defaults to false.
data: elementsThe HTML elements to patch.
1event: datastar-patch-elements
2data: elements <div id="foo">Hello world!</div>
3

Elements can be removed using the remove mode and providing a selector.

1event: datastar-patch-elements
2data: mode remove
3data: selector #foo
4

Elements can span multiple lines. Sample output showing non-default options:

1event: datastar-patch-elements
2data: mode inner
3data: selector #foo
4data: useViewTransition true
5data: elements <div>
6data: elements        Hello world!
7data: elements </div>
8

datastar-patch-signals #

Patches signals into the existing signals on the page. The onlyIfMissing line determines whether to update each signal with the new value only if a signal with that name does not yet exist. The signals line should be a valid data-signals attribute.

1event: datastar-patch-signals
2data: signals {foo: 1, bar: 2}
3

Signals can be removed by setting their values to null.

1event: datastar-patch-signals
2data: signals {foo: null, bar: null}
3

Sample output showing non-default options:

1event: datastar-patch-signals
2data: onlyIfMissing true
3data: signals {foo: 1, bar: 2}
4

SDKs

Datastar provides backend SDKs that can (optionally) simplify the process of generating SSE events specific to Datastar.

If you’d like to contribute an SDK, please follow the Contribution Guidelines.

Clojure #

A Clojure SDK as well as helper libraries and adapter implementations.

Maintainer: Jeremy Schoffen

Clojure SDK & examples

C# #

A C# (.NET) SDK for working with Datastar.

Maintainer: Greg H
Contributors: Ryan Riley

C# (.NET) SDK & examples

Go #

A Go SDK for working with Datastar.

Maintainer: Delaney Gillilan

Other examples: 1 App 5 Stacks ported to Go+Templ+Datastar

Go SDK & examples

Java #

A Java SDK for working with Datastar.

Maintainer: mailq
Contributors: Peter Humulock, Tom D.

Java SDK & examples

Kotlin #

A Kotlin SDK for working with Datastar.

Maintainer: GuillaumeTaffin

Kotlin SDK & examples

PHP #

A PHP SDK for working with Datastar.

Maintainer: Ben Croker

PHP SDK & examples

Craft CMS #

Integrates the Datastar framework with Craft CMS, allowing you to create reactive frontends driven by Twig templates.

Maintainer: Ben Croker (PutYourLightsOn)

Craft CMS plugin

Datastar & Craft CMS demos

Laravel #

Integrates the Datastar hypermedia framework with Laravel, allowing you to create reactive frontends driven by Blade views or controllers.

Maintainer: Ben Croker (PutYourLightsOn)

Laravel package

Python #

A Python SDK and a PyPI package (including support for most popular frameworks).

Maintainer: Felix Ingram
Contributors: Chase Sterling

Python SDK & examples

Ruby #

A Ruby SDK for working with Datastar.

Maintainer: Ismael Celis

Ruby SDK & examples

Rust #

A Rust SDK for working with Datastar.

Maintainer: Glen De Cauwsemaecker
Contributors: Johnathan Stevers

Rust SDK & examples

Rama #

Integrates Datastar with Rama, a Rust-based HTTP proxy (example).

Maintainer: Glen De Cauwsemaecker

Rama module

Scala #

ZIO HTTP #

Integrates the Datastar hypermedia framework with ZIO HTTP, a Scala framework.

Maintainer: Nabil Abdel-Hafeez

ZIO HTTP integration

TypeScript #

A TypeScript SDK with support for Node.js, Deno, and Bun.

Maintainer: Edu Wass
Contributors: Patrick Marchand

TypeScript SDK & examples

PocketPages #

Integrates the Datastar framework with PocketPages.

PocketPages plugin

Security

Datastar expressions are strings that are evaluated in a sandboxed context. This means you can use JavaScript in Datastar expressions.

Escape User Input #

The golden rule of security is to never trust user input. This is especially true when using Datastar expressions, which can execute arbitrary JavaScript. When using Datastar expressions, you should always escape user input. This helps prevent, among other issues, Cross-Site Scripting (XSS) attacks.

Avoid Sensitive Data #

Keep in mind that signal values are visible in the source code in plain text, and can be modified by the user before being sent in requests. For this reason, you should avoid leaking sensitive data in signals and always implement backend validation.

Ignore Unsafe Input #

If, for some reason, you cannot escape unsafe user input, you should ignore it using the data-ignore attribute. This tells Datastar to ignore an element and its descendants when processing DOM nodes.

Content Security Policy #

When using a Content Security Policy (CSP), unsafe-eval must be allowed for scripts, since Datastar evaluates expressions using a Function() constructor.

1<meta http-equiv="Content-Security-Policy" 
2    content="script-src 'self' 'unsafe-eval';"
3>