Skip to content

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.

Count: 0

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

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.

Order status · Idle

Start the stream to receive 3 updates, about 1.75 seconds apart. Production applications should not surprise users with unprompted movement on this cadence.

View status without live updates

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-required
  • Inline validation

    Server validates a field and streams field/error fragments; native validation stays first.

    datastar-required
  • Lazy tabs

    Selected content is application state, so the server returns the whole tab/panel fragment.

    datastar-required
  • Click to edit / CRUD

    Server renders display and edit variants, validates, then returns the authoritative row.

    datastar-required
  • Infinite scroll / lazy load

    Intersection or explicit Load more triggers an SSE fragment append.

    datastar-required
  • Progress and realtime status

    Long-lived SSE patches stable status elements as a job, order, or upload advances.

    datastar-required
  • File upload

    Use normal multipart forms for files; use signals only when base64 client-side handling is intentional.

    datastar-required
  • Dialogs and confirmation

    Native dialog owns focus; Datastar sends the command/result fragment.

    datastar-required
  • Filter, sort, and data views

    Signals describe the requested view; the server owns filtering, pagination, and permissions.

    datastar-required
  • Todo / bulk actions

    Mutations return fragments, letting the server reconcile counts, rows, and errors together.

    datastar-required
  • Custom events and optimistic UI

    Small DOM events compose with `thin:*` events; SSE remains the authority that confirms state.

    datastar-required
  • Polling, 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