Getting Started
Datastar brings the functionality provided by libraries like Alpine.js (frontend reactivity) and htmx (backend reactivity) together, into one cohesive solution. It’s a lightweight, extensible framework that allows you to:
- Manage state and build reactivity into your frontend using HTML attributes.
- Modify the DOM and state by sending events from your backend.
With Datastar, you can build any UI that a full-stack framework like React, Vue.js or Svelte can, but with a much simpler, hypermedia-driven approach.
Installation#
Using a Script Tag#
The quickest way to use Datastar is to include it in your HTML using a script tag hosted on a CDN.
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar/bundles/datastar.js"></script>
If you prefer to host the file yourself, download your own bundle using the bundler, then include it from the appropriate path.
<script type="module" src="/path/to/datastar.js"></script>
Using NPM#
You can alternatively install Datastar via npm. We don’t recommend this for most use-cases, as it requires a build step, but it can be useful for legacy frontend projects.
npm install @starfederation/datastar
Data Attributes#
At the core of Datastar are data-*
attributes (hence the name). They allow you to add reactivity to your frontend in a declarative way, and interact with your backend.
Datastar uses signals to manage state. You can think of signals as reactive variables that automatically track and propagate changes from expressions. They can be created and modified using data attributes on the frontend, and using events sent from the backend. Don’t worry if this sounds complicated; it will become clearer as we look at some examples.
data-model
#
Datastar provides us with a way to set up two-way data binding on an element using the data-model
attribute, which can be placed on any HTML element that users can directly input data or choices from (input
, textarea
, select
, checkbox
and radio
elements).
<input data-model="input" type="text" />
This creates a new signal called input
, and binds it to the element’s value. If either is changed, the other automatically updates.
data-text
#
To see this in action, we can use the data-text
attribute.
<div data-text="$input">
I will get replaced with the contents of the input signal
</div>
This sets the text content of an element to the value of the signal $input
. The $
is required to denote a signal in the expression.
The value of the data-text
attribute is an expression that is evaluated, meaning that we can use JavaScript in it.
<div data-text="$input.toUpperCase()">
Will be replaced with the uppercase contents of the input signal
</div>
data-computed-*
#
The data-computed-*
attribute creates a new 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.
<div data-computed-repeated="$input.repeat(2)">
<input data-model="input" type="text">
<div data-text="$repeated">
Will be replaced with the contents of the repeated signal
</div>
</div>
data-show
#
The data-show
attribute can be used to show or hide an element based on whether a JavaScript expression evaluates to true
or false
.
<button data-show="$input != ''">Save</button>
This results in the button being visible only when the input is not empty.
data-class
#
The data-class
attribute allows us to add or remove classes from an element using a set of key-value pairs that map to the class name and expression.
<button data-class="{hidden: $input == ''}">Save</button>
Since the expression evaluates to true
or false
, we can rewrite this as !$input
.
<button data-class="{hidden: !$input}">Save</button>
data-bind-*
#
The data-bind-*
attribute can be used to bind a JavaScript expression to any valid HTML attribute. The becomes even more powerful when combined with Web Components.
<button data-bind-disabled="$input == ''">Save</button>
This results in the button being given the disabled
attribute whenever the input is empty.
data-store
#
So far, we’ve created signals on the fly using data-model
and data-computed-*
. All signals are merged into a store that is accessible from anywhere in the DOM.
We can merge signals into the store using the data-store
attribute.
<div data-store="{input: ''}"></div>
The data-store
value must be written as a JavaScript object literal or using JSON syntax.
Adding data-store
to multiple elements is allowed, and the signals provided will be merged into the existing store (values defined later in the DOM tree override those defined earlier).
Signals are nestable, which can be useful for namespacing.
<div data-store="{primary: {input: ''}, secondary: {input: '' }}"></div>
data-on-*
#
The data-on-*
attribute can be used to execute a JavaScript expression whenever an event is triggered on an element.
<button data-on-click="$input=''">Reset</button>
This results in the $input
signal being set to an empty string when the button element is clicked. If the $input
signal is used elsewhere, its value will automatically update. This, like data-bind
can be used with any valid event name (e.g. data-on-keydown
, data-on-mouseover
, etc.).
So what else can we do with these expressions? Anything we want, really. See if you can follow the code below before trying the demo.
<div
data-store="{response: '', answer: 'bread'}"
data-computed-correct="$response.toLowerCase() == $answer"
>
<div id="question">
What do you put in a toaster?
</div>
<button data-on-click="$response = prompt('Answer:')">
BUZZ
</button>
<div data-show="$response != ''">
You answered “<span data-text="$response"></span>”.
<span data-show="$correct">That is correct ✅</span>
<span data-show="!$correct">
The correct answer is “
<span data-text="$answer"></span>
” 🤷
</span>
</div>
</div>
We’ve just scratched the surface of frontend reactivity. Now let’s take a look at how we can bring the backend into play.
Backend Setup#
Datastar uses Server-Sent Events or SSE. There’s no special backend plumbing required to use SSE, just some special syntax. Fortunately, SSE is straightforward and provides us with some advantages.
First, set up your backend in the language of your choice. Using one of the helper SDKs (available for Go, PHP and TypeScript) will help you get up and running faster. We’re going to use the SDKs in the examples below, which set the appropriate headers and format the events for us, but this is optional.
The following code would exist in a controller action endpoint in your backend.
using StarFederation.Datastar.DependencyInjection;
// add as a service
builder.Services.AddDatastar();
app.MapGet("/", async (IServerSentEventGenerator sse) =>
{
// Merges HTML fragments into the DOM.
await sse.MergeFragments(@"<div id=""question"">What do you put in a toaster?</div>");
// Merges signals into the store.
await sse.MergeSignals("{response: '', answer: 'bread'}");
});
import (datastar "github.com/starfederation/datastar/code/go/sdk")
// Creates a new `ServerSentEventGenerator` instance.
sse := datastar.NewSSE(w,r)
// Merges HTML fragments into the DOM.
sse.MergeFragments(
`<div id="question">What do you put in a toaster?</div>`
)
// Merges signals into the store.
sse.MergeSignals(`{response: '', answer: 'bread'}`)
use starfederation\datastar\ServerSentEventGenerator;
// Creates a new `ServerSentEventGenerator` instance.
$sse = new ServerSentEventGenerator();
// Merges HTML fragments into the DOM.
$sse->mergeFragments(
'<div id="question">What do you put in a toaster?</div>'
);
// Merges signals into the store.
$sse->mergeSignals(['response' => '', 'answer' => 'bread']);
The mergeFragments()
method merges the provided HTML fragment into the DOM, replacing the element with id="question"
. An element with the ID question
must already exist in the DOM.
The mergeSignals()
method merges the response
and answer
signals into the frontend store.
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.
<div
data-store="{response: '', answer: '', correct: false}"
data-computed-correct="$response.toLowerCase() == $answer"
>
<div id="question"></div>
<button data-on-click="$get('/actions/quiz')">
Fetch a question
</button>
<button data-show="$answer != ''"
data-on-click="$response = prompt('Answer:') ?? ''"
>
BUZZ
</button>
<div data-show="$response != ''">
You answered “<span data-text="$response"></span>”.
<span data-show="$correct">That is correct ✅</span>
<span data-show="!$correct">
The correct answer is “<span data-text="$answer2"></span>” 🤷
</span>
</div>
</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!
data-indicator
#
The data-indicator
attribute sets the value of the provided signal name to true
while the request is in flight. We can use this signal to show a loading indicator, which may be desirable for slower responses.
Note that elements using the data-indicator
attribute must have a unique ID attribute.
<div id="question"></div>
<div data-class="{loading: $fetching}" class="indicator"></div>
<button id="fetch-a-question"
data-on-click="$get('/actions/quiz')"
data-indicator="fetching"
>
Fetch a question
</button>
We’re not limited to just GET
requests. We can also send GET
, POST
, PUT
, PATCH
and DELETE
requests, using the $get()
, $post()
, $put()
, $patch()
and $delete()
actions, respectively.
Here’s how we could send an answer to the server for processing, using a POST
request.
<button data-on-click="$post('/actions/quiz')">
Submit answer
</button>
One of the benefits of using SSE is that we can send multiple events (HTML fragments, signal updates, etc.) in a single response.
sse.MergeFragments(@"<div id=""question"">...</div>");
sse.MergeFragments(@"<div id=""instructions"">...</div>");
sse.MergeSignals("{answer: '...'}");
sse.MergeSignals("{prize: '...'}");
sse.MergeFragments(`<div id="question">...</div>`)
sse.MergeFragments(`<div id="instructions">...</div>`)
sse.MergeSignals(`{answer: '...'}`)
sse.MergeSignals(`{prize: '...'}`)
$sse->mergeFragments('<div id="question">...</div>');
$sse->mergeFragments('<div id="instructions">...</div>');
$sse->mergeSignals(['answer' => '...']);
$sse->mergeSignals(['prize' => '...']);
Actions#
Actions in Datastar are helper functions that are available in data-*
attributes and have the syntax $actionName()
. We already saw the $get
action above. Here are a few other common actions.
$setAll()
#
The $setAll()
action sets the values of multiple signals at once. It takes a regular expression that is used to match against signals, and a value to set them to, as arguments.
<button data-on-click="$setAll('form_', true)"></button>
This sets the values of all signals containing form_
to true
, which could be useful for enabling input fields in a form.
<input type="checkbox" data-model="checkbox_1"> Checkbox 1
<input type="checkbox" data-model="checkbox_2"> Checkbox 2
<input type="checkbox" data-model="checkbox_3"> Checkbox 3
<button data-on-click="$setAll('checkbox_', true)">Check All</button>
$toggleAll()
#
The $toggleAll()
action toggles the values of multiple signals at once. It takes a regular expression that is used to match against signals, as an argument.
<button data-on-click="$toggleAll('form_')"></button>
This toggles the values of all signals containing form_
(to either true
or false
), which could be useful for toggling input fields in a form.
<input type="checkbox" data-model="checkbox_1"> Checkbox 1
<input type="checkbox" data-model="checkbox_2"> Checkbox 2
<input type="checkbox" data-model="checkbox_3"> Checkbox 3
<button data-on-click="$toggleAll('checkbox_')">Toggle All</button>
A Quick Overview#
Using data-*
attributes, you can introduce reactive state to your frontend and access it anywhere in the DOM and in your backend. You can set up events that trigger requests to backend endpoints that respond with HTML fragment and signal updates.
- Bind element values to signals:
data-model="foo"
- Set the text content of an element to an expression.:
data-text="$foo"
- Create a computed signal:
data-computed-foo="$bar + 1"
- Show or hide an element using an expression: `data-show=“$foo”
- Modify the classes on an element:
data-class="{'font-bold': $foo}"
- Bind an expression to an HTML attribute:
data-bind-disabled="$foo == ''"
- Merge signals into the store:
data-store="{foo: ''}"
- Execute an expression on an event:
data-on-click="$get(/endpoint)"
- Use signals to track in flight backend requests:
data-indicator="fetching"
- Replace the URL:
data-replace-url="'/page1'"
- Persist all signals in local storage:
data-persist
- Create a reference to an element:
data-ref="alert"
- Check for intersection with the viewport:
data-intersect="alert('visible')"
- Scroll programmatically:
data-scroll-into-view
- Interact with the View Transition API:
data-transition="slide"