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 bundleInclude 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 iserroruploadProgress(): 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
RequestInitoptions (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 iserroroptions(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.hsFetch?.state()"></div>
</form>- All HTTP methods (
GET,POST,PUT,DELETE, etc.) - Multiple encoding types:
application/x-www-form-urlencoded(default)multipart/form-dataapplication/json
- Progress tracking for uploads and downloads
- Automatic error handling
Each hijacked form has a hsFetch 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();