A hypermedia framework inspired by datastar and HTMX, implementing reactive signals and HTML-driven interactions with a minimal footprint.
HyperStim uses reactive signals to manage state and data attributes to define behavior directly in HTML. When a signal is declared, it becomes globally accessible and automatically updates any dependent elements when changed.
The core pattern:
- Declare signals with
data-signals-name="value"
- React to changes with
data-effect="expression"
- Bind form inputs with
data-bind="signalName()"
- Handle events with
data-on-event="expression"
Signals are automatically created and made available globally, so data-signals-counter="0"
creates a counter()
function usable anywhere in HTML.
HyperStim encourages self-hosting and does not provide a CDN. Build from source:
deno task bundle
Include HyperStim in HTML:
<script type="module" src="dist/hyperstim.min.js"></script>
Perform HTTP requests with built-in progress tracking and response handling.
<div data-signals-api="fetch('/api/data')"></div>
<button data-on-click="api().trigger()">Load Data</button>
<!-- Monitor state -->
<div data-effect="this.textContent = api().state()"></div>
<div data-effect="this.style.display = api().state() === 'pending' ? 'block' : 'none'">
Loading...
</div>
fetch(resource, options)
resource
(string|URL): The URL to fetchoptions
(object, optional): Request optionsmethod
: HTTP method (GET, POST, etc.)headers
: Request headers objectbody
: Request bodytimeout
: Request timeout in millisecondsonOther
(function): Handler for custom commands- Plus other standard fetch options
The fetch action returns an object with the following properties:
state()
: Returns current state of the fetch actionerror()
: Returns error details when state iserror
uploadProgress()
: Returns upload progress{ loaded, total, percent, lengthComputable }
downloadProgress()
: Returns download progress{ loaded, total, percent, lengthComputable }
options(newOptions)
: Get/set request options (method, headers, body, etc.)resource(newUrl)
: Get/set the request URLtrigger()
: Execute the request and return the action objectabort()
: Cancel the current request
initial
: Action created but not yet triggeredpending
: Request in progresssuccess
: Request completed successfullyerror
: Request failed (checkerror()
for details)aborted
: Request was cancelled before completion
HyperStim automatically processes JSON responses containing commands that update signals, patch DOM elements, or execute JavaScript.
<div data-signals-api="fetch('/api/data')"></div>
<button data-on-click="api().trigger()">Load Data</button>
{"type": "hs-patch-signals", "counter": 42, "username": "Alice"}
Real-time updates via Server-Sent Events.
<div data-signals-stream="sse('/events')"></div>
<button data-on-click="stream().connect()">Connect</button>
<button data-on-click="stream().close()">Disconnect</button>
<!-- Monitor connection state -->
<div data-effect="this.textContent = stream().state()"></div>
sse(url, options)
url
(string|URL): The Server-Sent Events endpoint URLoptions
(object, optional): SSE connection optionsopenWhenHidden
(boolean): Whether to keep connection open when page is hidden (default: false)onOther
(function): Handler for custom commands- Plus all standard
RequestInit
options (method, headers, credentials, etc.)
The sse action returns an object with the following properties:
state()
: Returns connection state (initial
,connecting
,connected
,error
,closed
)error()
: Returns error details when state iserror
options(newOptions)
: Get/set SSE connection optionsresource(newUrl)
: Get/set the SSE endpoint URLconnect()
: Establish SSE connection and return the action objectclose()
: Close the SSE connection
initial
: Stream created but not connectedconnecting
: Attempting to establish connectionconnected
: Successfully connected and receiving eventserror
: Connection failed (checkerror()
for details)closed
: Connection closed
SSE endpoints send commands as events where the event name determines the command type.
<div data-signals-stream="sse('/events')"></div>
<button data-on-click="stream().connect()">Connect</button>
event: hs-patch-signals
data: {"counter": 42, "username": "Alice"}
Both fetch and SSE actions process commands that update signals, patch DOM elements, or execute JavaScript.
Updates signal values. Any properties other than type
become signal updates.
{
"type": "hs-patch-signals",
"counter": 42,
"username": "Alice"
}
Patches DOM elements. Requires html
content, patchTarget
CSS selector, and patchMode
.
{
"type": "hs-patch-html",
"html": "<p>New content</p>",
"patchTarget": "#container",
"patchMode": "append"
}
HTML patches support different modes:
inner
: Replace element content (default)outer
: Replace the entire elementappend
: Append to element contentprepend
: Prepend to element contentbefore
: Insert before the elementafter
: Insert after the element
Executes JavaScript expressions. The code
property contains the JavaScript to run.
{
"type": "hs-execute",
"code": "console.log('Hello from server!')"
}
Multiple commands can be sent as an array in fetch responses:
[
{
"type": "hs-patch-signals",
"counter": 1
},
{
"type": "hs-patch-html",
"html": "<div>Updated</div>",
"patchTarget": "#status",
"patchMode": "inner"
}
]
For SSE, the command type is specified as the event name:
event: hs-patch-signals
data: { "counter": 1 }
event: hs-patch-html
data: { "html": "<div>Updated</div>", "patchTarget": "#status", "patchMode": "inner" }
Custom commands can be handled using the onOther
option:
<div data-signals-api="fetch('/api/data', {
onOther: (command) => {
console.log('Received custom command:', command.type);
// ...
}
})"></div>
<div data-signals-stream="sse('/events', {
onOther: (command) => {
console.log('Received custom command:', command.type);
// ...
}
})"></div>
HyperStim automatically hijacks forms with the data-hijack
attribute, converting them to AJAX submissions.
<form action="/submit" method="post" data-hijack>
<input name="username" type="text">
<input name="email" type="email">
<button type="submit">Submit</button>
<!-- Monitor form submission state -->
<div data-effect="this.textContent = this.form.__hyperstim_action?.state()"></div>
</form>
- All HTTP methods (
GET
,POST
,PUT
,DELETE
, etc.) - Multiple encoding types:
application/x-www-form-urlencoded
(default)multipart/form-data
application/json
- Progress tracking for uploads and downloads
- Automatic error handling
Each hijacked form has a __hyperstim_action
property that contains a regular fetch action object with all the same properties and methods documented above.
Declare reactive signals.
data-signals-{name}="{expression}"
- Create named signaldata-signals="{object}"
- Create multiple signals from object
Run expressions reactively when dependencies change. Multiple expressions separated by commas.
data-effect="{expression1}, {expression2}"
Create derived signals from other signals. When using multiple expressions, only the last expression's value is assigned.
data-computed-{name}="{expression1}, {expression2}"
Execute expressions once when the element is first processed.
data-init="{expression}"
Two-way binding between form controls and signals.
data-bind="{signalName}()"
Supports input
, textarea
, and select
elements.
Handle DOM events with optional modifiers.
data-on-{event}[__{modifier}]="{expression}"
Event handling supports optional modifiers:
- Timing:
debounce.{time}
(wait for pause),throttle.{time}
(limit frequency),delay.{ms}
(postpone execution) - Conditions:
trusted
(user-initiated only),once
(fire only once),outside
(when clicking outside element) - Event handling:
prevent
(preventDefault),stop
(stopPropagation),passive
(non-blocking),capture
(capture phase) - Targeting:
window
(attach to window instead of element)
All data-attribute expressions have access to the functionality exposed in globalThis.HyperStim
, which is automatically spread into the expression context:
- Declared signals by name - From
HyperStim.signals.*
(e.g.,counter()
for a signal declared asdata-signals-counter
) - Action functions - From
HyperStim.actions.*
(fetch()
,sse()
) - Builtin functions - From
HyperStim.builtin
(builtin.signal()
,builtin.effect()
,builtin.computed()
)
Signals declared with data-signals-name
and computed signals declared with data-computed-name
become accessible as HyperStim.signals.name
. Regular signals accept no arguments to read, one argument to write. Computed signals are read-only.
Actions can be created programmatically and return the same objects documented above with their respective properties and methods.
HyperStim.actions.fetch(resource, options)
- Creates a fetch actionHyperStim.actions.sse(url, options)
- Creates a SSE action
HyperStim.builtin
functions create signals, effects, and computed values programmatically:
HyperStim.builtin.signal(value)
- Creates reactive signalHyperStim.builtin.effect(fn)
- Creates reactive effectHyperStim.builtin.computed(fn)
- Creates computed signal
// Creates a signal
const count = HyperStim.builtin.signal(0);
// Creates an effect that runs when `count` changes
const dispose = HyperStim.builtin.effect(() => {
console.log('Count is:', count());
});
// Creates a computed signal
const doubled = HyperStim.builtin.computed(() => count() * 2);
// Updates the signal
count(5); // Effect logs: "Count is: 5", doubled() now returns 10
// Disposes the effect to stop it from running
dispose();