Actions

Datastar provides actions 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(url: string, options={ })

Sends a GET request to the backend using fetch. The URL can be any valid URL 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: *} 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 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 speciying 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(url: 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(url: 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(url: 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(url: 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<div data-on-click="@get('/endpoint', {
2    filterSignals: {include: /^foo\./},
3    headers: {
4        'X-Csrf-Token': 'JImikTbsoCYQ9oGOcvugov0Awc5LbqFsZW6ObRCxuq',
5    },
6    openWhenHidden: true,
7})"></div>

Request Cancellation #

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 -->
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.

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 = '<div>New content</div>'

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>

Upload Progress #Pro

All backend actions (@get(), @post(), @put(), @patch(), @delete()) automatically support file upload progress monitoring when:

The HTTPS requirement exists due to browser security restrictions. Browsers only allow the duplex option (required for ReadableStream uploads) on secure connections. For HTTP URLs or non-FormData requests, standard fetch is used without progress tracking.

When these conditions are met, the actions dispatch upload-progress fetch events with:

 1<form enctype="multipart/form-data"
 2    data-signals="{progress: 0, uploading: false}"
 3    data-on-submit__prevent="@post('https://example.com/upload', {contentType: 'form'})"
 4    data-on-datastar-fetch="
 5        if (evt.detail.type !== 'upload-progress') return;
 6
 7        const {progress, loaded, total} = evt.detail.argsRaw;
 8        $uploading = true;
 9        $progress = Number(progress);
10
11        if ($progress >= 100) {
12            $uploading = false;
13        }
14    "
15>
16    <input type="file" name="files" multiple />
17    <button type="submit">Upload</button>
18    <progress data-show="$uploading" data-attr-value="$progress" max="100"></progress>
19</form>

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 be 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>