.agents/skills/datastar/SKILL.md
Hypermedia framework for building reactive web applications with backend-driven UI. Use this skill for Datastar development patterns, SSE streaming, signal management, DOM morphing, and the "Datastar way" philosophy. Covers data-* attributes, backend integration, and real-time collaborative app patterns.
npx skillsauth add dodyg/blue-nile-pds datastarInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Datastar is a lightweight (~11KB) hypermedia framework for building reactive web applications. It enables server-side rendering with frontend framework capabilities by accepting HTML and SSE responses. The backend drives the frontend - this is the core philosophy.
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js"></script>
The "Datastar way" is a set of opinions from the core team on building maintainable, scalable, high-performance web apps.
Backend is Source of Truth: Most application state should reside on the server. The frontend is exposed to users and cannot be trusted.
Sensible Defaults: Resist the urge to customize before questioning whether changes are truly necessary.
DOM Patching Over Fine-Grained Updates: The backend drives frontend changes by patching HTML elements and signals. Send large DOM chunks ("fat morph") efficiently.
Morphing as Core Strategy: Only modified parts of the DOM are updated, preserving state and improving performance.
Restrained Signal Usage: Signals should serve specific purposes:
Datastar leverages Server-Sent Events (SSE) as its foundation. The server pushes data through a persistent connection.
Key insight: Datastar extends text/event-stream beyond SSE's GET-only limitation. HTML fragments wrapped in this protocol support POST, PUT, PATCH, DELETE operations.
_-prefixed) and sends requestdatastar-patch-elementsModifies DOM elements through morphing.
event: datastar-patch-elements
data: mode inner
data: selector #target
data: elements <div id="foo">Hello world!</div>
Options:
selector - CSS selector for target elementmode - outer (default), inner, replace, prepend, append, before, after, removeuseViewTransition - Enable view transitionsMultiline Elements: Each line must be prefixed with elements:
event: datastar-patch-elements
data: mode inner
data: selector #target
data: elements <div>
data: elements <span>Line 1</span>
data: elements <span>Line 2</span>
data: elements </div>
datastar-patch-signalsUpdates reactive state on the page.
event: datastar-patch-signals
data: signals {"foo": 1, "bar": 2}
Remove signals by setting to null.
data-on:click - Handle click eventsdata-on:submit - Handle form submissionsdata-on:keydown__window - Global key events (with __window modifier)data-on-intersect - Visibility triggersdata-on-interval - Periodic triggersdata-signals - Define signals: data-signals="{count: 0}"data-bind - Two-way binding: data-bind="email" (NOT data-bind:value="email")data-computed - Derived valuesdata-init - Run expression on load (NOT data-on-load)data-text - Set text content: data-text="$count"data-show - Conditional visibility: data-show="$isVisible"data-class - Dynamic classesdata-attr - Dynamic attributes: data-attr:src="$imageUrl"data-ref - Element referencesdata-ignore - Skip this element during morphdata-ignore-morph - Preserve element across morphsdata-preserve-attr - Keep specific attributes@get('/path') - GET request@post('/path') - POST request@put('/path') - PUT request@patch('/path') - PATCH request@delete('/path') - DELETE request@peek() - Access signals without subscribing@setAll() - Set all matching signals@toggleAll() - Toggle boolean signalsCRITICAL: In expressions, signals MUST be prefixed with $:
<!-- CORRECT -->
<span data-text="$count"></span>
<div data-show="$isVisible"></div>
<img data-attr:src="$imageUrl">
<span data-text="`Loaded ${$count} items`"></span>
<!-- WRONG - Missing $ prefix -->
<span data-text="count"></span>
<div data-show="isVisible"></div>
_-prefixed signals: LOCAL ONLY, NOT sent to backend<div data-signals="{username: '', _isMenuOpen: false}">
<!-- username goes to backend, _isMenuOpen stays local -->
</div>
IMPORTANT: Do NOT use _ prefix for signals that need to be sent to the backend (like form values). Use regular names for data that must reach the server.
The signal name goes in the VALUE, not as a key suffix:
<!-- CORRECT -->
<input data-bind="email">
<input data-bind="username">
<!-- WRONG -->
<input data-bind:value="email">
All SDKs provide helpers for reading signals and streaming responses:
// ASP.NET Core example
public static async Task GetFeedsAsync(
HttpResponse response,
SqliteConnectionFactory connectionFactory,
CancellationToken cancellationToken)
{
// Build HTML content
StringBuilder html = new();
html.AppendLine("<div class=\"feed-item\">...</div>");
// Stream SSE response
SseHelper sse = response.CreateSseHelper();
await sse.StartAsync(cancellationToken);
await sse.PatchElementsAsync(html.ToString(), "#target", "inner", cancellationToken);
}
Available SDKs: Go, Python, TypeScript, PHP, Ruby, Rust, Java, Kotlin, Scala, C#, Clojure
When writing SSE responses, ensure:
text/event-streamnew UTF8Encoding(false) in C#)Connection, Transfer-Encoding, Keep-Alive, Upgrade, or Proxy-Connection headers - these are invalid for HTTP/2 and HTTP/3 and will cause Kestrel warningsevent: datastar-patch-elements
data: mode inner
data: selector #source-filters
data: elements <div class="item">
data: elements Content here
data: elements </div>
data: elements <div class="item">
data: elements More content
data: elements </div>
<div data-signals="{email: '', password: ''}">
<input type="email" data-bind="email">
<input type="password" data-bind="password">
<button data-on:click="@post('/login')">Login</button>
</div>
<div data-init="@get('/feeds'); @get('/items')">
<!-- Content loaded on page init -->
</div>
<div data-signals="{_showDetails: false}">
<button data-on:click="_showDetails = !_showDetails">Toggle</button>
<div data-show="$_showDetails">
Details here...
</div>
</div>
<button data-on:click="@post('/submit')"
data-indicator="fetching"
data-attr:disabled="$fetching">
Submit
</button>
<div data-show="$fetching">Loading...</div>
<span data-text="`Loaded ${$count} items`"></span>
<span data-text="`Hello, ${$username}!`"></span>
<div data-on-interval="1000; @get('/status')">
<!-- Refreshes every second -->
</div>
<div data-on-intersect="@get('/load-more')">
Loading...
</div>
Use native JavaScript confirm() - NOT @confirm which doesn't exist:
<!-- CORRECT: Use native JS confirm() -->
<button data-on:click="confirm('Delete this item?') && @delete('/items/123')">
Delete
</button>
<!-- WRONG: @confirm is not a valid action -->
<button data-on:click="@confirm('Delete?') && @delete('/items/123')">
Delete
</button>
Segregate commands (writes) from queries (reads):
This enables real-time collaboration patterns.
data-on:input__debounce.500ms for search fieldsCRITICAL: Datastar sends signals differently depending on the HTTP method:
datastar query parameter as URL-encoded JSONpublic static async Task<Dictionary<string, JsonElement>> ReadSignalsAsync(this HttpRequest request)
{
// GET requests send signals via query parameter
string? datastarParam = request.Query["datastar"];
if (!string.IsNullOrWhiteSpace(datastarParam))
{
return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(datastarParam) ?? [];
}
// POST/PUT/PATCH/DELETE send signals in body
using StreamReader reader = new(request.Body, Encoding.UTF8);
string body = await reader.ReadToEndAsync();
return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(body) ?? [];
}
Example GET request URL:
GET /river/items?reset=true&datastar=%7B%22selectedFeedIds%22%3A%22abc123%22%7D
$ prefix in expressions: Always use $signalName in data-text, data-show, data-attr, etc.data-bind="signalName", not data-bind:value="signalName"data-on-load: Use data-init instead_ are NOT sent to the backend@confirm action: @confirm is NOT a valid Datastar action. Use native JavaScript confirm() instead:
<!-- WRONG -->
<button data-on:click="@confirm('Delete?') && @delete('/item/1')">Delete</button>
<!-- CORRECT -->
<button data-on:click="confirm('Delete?') && @delete('/item/1')">Delete</button>
datastar query parameter, NOT the request body. Always check both locations.response.Headers.Connection = "keep-alive" in SSE responses. This header is invalid for HTTP/2 and HTTP/3 - Kestrel handles keep-alive automatically and will emit warnings if these headers are present.data-init on dynamically added elements: data-init on elements added via SSE patching is unreliable. Datastar's morphing may not properly execute data-init on dynamically appended content. Instead, the backend should directly render and patch the updated HTML fragments:
// WRONG: Trying to trigger follow-up requests via data-init
await sse.PatchElementsAsync(
"<div data-init=\"@get('/feeds'); @get('/items')\"></div>",
"body", "append", cancellationToken);
// CORRECT: Directly render and patch the updated content
string feedsHtml = await BuildFeedsHtmlAsync(connectionFactory, cancellationToken);
await sse.PatchElementsAsync(feedsHtml, "#source-filters", "inner", cancellationToken);
string itemsHtml = await BuildItemsHtmlAsync(signals, connectionFactory, cancellationToken);
await sse.PatchElementsAsync(itemsHtml, "#items", "inner", cancellationToken);
testing
Get best practices for TUnit unit testing, including data-driven tests
development
Severity scoring, scorecard computation, confidence levels, and remediation tracking for web accessibility audits. Use when computing page accessibility scores (0-100 with A-F grades), tracking remediation progress across audits, or generating cross-page comparison scorecards.
development
Web content discovery, URL crawling, and page inventory for accessibility audits. Use when scanning web pages, crawling sites for audit scope, or building page inventories for multi-page audits.
development
Audit report formatting, severity scoring, scorecard computation, and compliance export for document accessibility audits. Use when generating DOCUMENT-ACCESSIBILITY-AUDIT.md reports, computing document severity scores (0-100 with A-F grades), creating VPAT/ACR compliance exports, or formatting remediation priorities.