Advanced / Datastar + SSE
Server-authoritative interactions, documented as components and recipes.
Datastar-required recipes load the pinned v1.0.2 browser asset. Local signals are useful for transient presentation; persistent state, permissions, validation, and workflow effects belong to the server and return HTML or SSE patches.
Local signals
This runs in the static docs now. It is intentionally local-only: no durable state is implied.
<section data-signals="{count: 0, name: ''}">
<button class="thin-button" data-on:click="$count++">Increment</button>
<output data-text="'Count: ' + $count">Count: 0</output>
<label>Name <input data-bind:name>
</label>
<p data-show="$name" data-text="'Hello, ' + $name"></p>
</section>Show code
<section data-signals="{count: 0, name: ''}">
<button class="thin-button" data-on:click="$count++">Increment</button>
<output data-text="'Count: ' + $count">Count: 0</output>
<label>Name <input data-bind:name>
</label>
<p data-show="$name" data-text="'Hello, ' + $name"></p>
</section>Active search
Type to call a real Astro endpoint; the server filters its catalog and streams the authoritative results fragment.
All 4 available products. Start typing to narrow the list.
- Aurora field jacket
- Harbor canvas tote
- Northline trail shoes
- Paper everyday notebook
<search aria-label="Product search">
<form id="product-search" data-signals="{q: ''}" data-on:input__debounce.250ms="@get('/api/advanced/search')">
<label class="thin-field">Search <input name="q" type="search" list="search-products" data-bind:q>
</label>
<datalist id="search-products">
<option value="Aurora field jacket">
</datalist>
<button type="submit">Search</button>
</form>
</search>
<section id="search-results" aria-live="polite">All available products…</section>
<!-- Server streams a datastar fragment replacing #search-results. -->Show code
<search aria-label="Product search">
<form id="product-search" data-signals="{q: ''}" data-on:input__debounce.250ms="@get('/api/advanced/search')">
<label class="thin-field">Search <input name="q" type="search" list="search-products" data-bind:q>
</label>
<datalist id="search-products">
<option value="Aurora field jacket">
</datalist>
<button type="submit">Search</button>
</form>
</search>
<section id="search-results" aria-live="polite">All available products…</section>
<!-- Server streams a datastar fragment replacing #search-results. -->Lazy server tabs
Use Thin-Sky tabs for keyboard mechanics; let the server own which panel content is selected and rendered.
Details
Server-rendered product details remain authoritative and linkable.
Reviews
Rated 4.8 out of 5 from 126 verified reviews.
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product information">
<button role="tab" aria-controls="panel-details" aria-selected="true" data-on:click="@get('/api/advanced/tabs?tab=details')">Details</button>
<button role="tab" aria-controls="panel-reviews" aria-selected="false" data-on:click="@get('/api/advanced/tabs?tab=reviews')">Reviews</button>
</div>
<section id="panel-details" role="tabpanel">Initial server-rendered details.</section>
<section id="panel-reviews" role="tabpanel" hidden>Server-rendered reviews.</section>Show code
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product information">
<button role="tab" aria-controls="panel-details" aria-selected="true" data-on:click="@get('/api/advanced/tabs?tab=details')">Details</button>
<button role="tab" aria-controls="panel-reviews" aria-selected="false" data-on:click="@get('/api/advanced/tabs?tab=reviews')">Reviews</button>
</div>
<section id="panel-details" role="tabpanel">Initial server-rendered details.</section>
<section id="panel-reviews" role="tabpanel" hidden>Server-rendered reviews.</section>SSE status stream
This is the server half of a long-lived status channel. The browser receives Datastar patch events, not a client-owned state store. The demo interval (~1.75s) exists only to illustrate transport—do not treat periodic SSE polling as a library or product default; prefer explicit user-initiated refresh or genuinely event-driven server pushes.
Start the stream to receive 3 updates, about 1.75 seconds apart. Production applications should not surprise users with unprompted movement on this cadence.
import type { APIRoute } from "astro";
import { createDatastarResponse } from "../../lib/datastar-sdk";
// Keep state authoritative and stream Datastar patches.
export const GET: APIRoute = ({ request }) => createDatastarResponse(
request.signal,
(stream) => {
stream.patchElements(
`<section id="order-status"><p>Picking · 2 of 4 items</p></section>`
);
// Later, when the order changes:
stream.patchElements(
`<section id="order-status"><p>Shipped · tracking available</p></section>`
);
},
);Show code
import type { APIRoute } from "astro";
import { createDatastarResponse } from "../../lib/datastar-sdk";
// Keep state authoritative and stream Datastar patches.
export const GET: APIRoute = ({ request }) => createDatastarResponse(
request.signal,
(stream) => {
stream.patchElements(
`<section id="order-status"><p>Picking · 2 of 4 items</p></section>`
);
// Later, when the order changes:
stream.patchElements(
`<section id="order-status"><p>Shipped · tracking available</p></section>`
);
},
);Recipe coverage from the Datastar examples
These are Thin-Sky recipes—not generic client widgets. Each needs server routes, response fragments, and fallback HTML before being promoted from Lab.
Active search
Debounced signal request; server replaces a live results region.
datastar-requiredInline validation
Server validates a field and streams field/error fragments; native validation stays first.
datastar-requiredLazy tabs
Selected content is application state, so the server returns the whole tab/panel fragment.
datastar-requiredClick to edit / CRUD
Server renders display and edit variants, validates, then returns the authoritative row.
datastar-requiredInfinite scroll / lazy load
Intersection or explicit Load more triggers an SSE fragment append.
datastar-requiredProgress and realtime status
Long-lived SSE patches stable status elements as a job, order, or upload advances.
datastar-requiredFile upload
Use normal multipart forms for files; use signals only when base64 client-side handling is intentional.
datastar-requiredDialogs and confirmation
Native dialog owns focus; Datastar sends the command/result fragment.
datastar-requiredFilter, sort, and data views
Signals describe the requested view; the server owns filtering, pagination, and permissions.
datastar-requiredTodo / bulk actions
Mutations return fragments, letting the server reconcile counts, rows, and errors together.
datastar-requiredCustom events and optimistic UI
Small DOM events compose with `thin:*` events; SSE remains the authority that confirms state.
datastar-requiredPolling, intervals, and view transitions
Use sparingly for genuinely changing data, with stable ids and user-visible failure states. Documentation demo intervals are not product defaults.
datastar-required