∴ sprae
DOM microhydration
Reactive sprinkles for HTML/JSX
<div :scope="{ q: '', items: ['Apple', 'Apricot', 'Banana', 'Cherry', 'Date', 'Elderberry'] }">
<input :value="q" :change="v => q = v" placeholder="Search fruits..." />
<ul>
<li :each="item in items.filter(i => i.includes(q))" :text="item"></li>
</ul>
</div>
Principles
- HTML-native
- Keep existing HTML.
Standard JS expressions.
No build step, no config. - Open & pluggable
- Controllable state. ESM-first.
Signals-powered reactivity.
Sandboxed. CSP-safe eval. - 5kb, 0 deps
- One
<script>tag ornpm i.
Any backend, any template, +JSX.
No ecosystem lock-in.
Usage
Add one script tag. Sprae evaluates : attributes and makes reactivity.
<script src="//unpkg.com/sprae" data-start></script>
Variants:
<!-- CSP-safe (no eval) -->
<script src="//unpkg.com/sprae/dist/sprae-csp.umd.js" data-start></script>
<!-- Preact signals -->
<script src="//unpkg.com/sprae/dist/sprae-preact.umd.js" data-start></script>
Install or download sprae.js and import:
<script type="module">
import sprae from './sprae.js'
const state = sprae(document.getElementById('app'), { count: 0 })
state.count++ // updates DOM
</script>
Variants: sprae-csp.js (CSP-safe), sprae-preact.js (preact signals).
Reference Docs →
| directive | description | example |
|---|---|---|
:text |
Set text content | <span :text="name"> |
:html |
Set innerHTML | <div :html="content"> |
:class |
Set classes | <div :class="{active: true}"> |
:style |
Set styles | <div :style="{color:'#fff'}"> |
:value |
Bind input (state→DOM) | <input :value="text"> |
:change |
Write input back (DOM→state) | <input :change="v => text = v"> |
:<prop> |
Set any attribute | <a :href="url"> |
:hidden |
Toggle visibility | <div :hidden="!show"> |
:if :else |
Conditional render | <div :if="cond"> |
:each |
List render | <li :each="item in list"> |
:scope |
Create local state | <div :scope="{x:1}"> |
:ref |
Element reference | <input :ref="name"> |
:mount |
Connect/cleanup hook | <canvas :mount="el => init(el)"> |
:intersect |
Visibility observer | <img :intersect.once="load()"> |
:resize |
Size observer | <div :resize="({width}) => ..."> |
:fx |
Side effect | <div :fx="log(x)"> |
:on<event> |
Event listener | <button :onclick="fn()"> |
:portal |
Move to container | <div :portal="'#modals'"> |
| modifier | description | example |
|---|---|---|
.debounce |
Delay until activity stops | :oninput.debounce-300 |
.throttle |
Limit call frequency | :onscroll.throttle-100 |
.delay |
Delay each call | :onmouseenter.delay-500 |
.once |
Run only once | :onclick.once |
.window |
Listen on window | :onkeydown.window |
.document |
Listen on document | :onclick.document |
.body .root .parent |
Other targets | :onclick.parent |
.self |
Only direct target | :onclick.self |
.away |
Click outside element | :onclick.away |
.prevent |
Prevent default | :onclick.prevent |
.stop |
Stop propagation | :onclick.stop |
.passive .capture |
Listener options | :onscroll.passive |
.enter .esc .tab .space |
Common keys | :onkeydown.enter |
.ctrl .shift .alt .meta |
Modifier keys | :onkeydown.ctrl-s |
.arrow .digit .letter .delete |
Key groups | :onkeydown.digit |
FAQ All questions →
- What is it?
- A ~5kb script that adds reactivity to HTML via
:attribute="expression". No build step, no new syntax — just HTML and JS you already know. - When to use it?
- Adding interactivity to server-rendered pages, static sites, prototypes, or anywhere a full framework is overkill. Works with any backend — Rails, Django, PHP, Jekyll, Next.js.
- How does it compare?
- 3x lighter than Alpine, faster in benchmarks. Signals-powered (emerging standard). Full comparison.
- Components?
- Use define-element for declarative web components, or any CE library.
- Is it production-ready?
- 3+ years, 12 major versions. Used by a few SaaS systems and landing pages. Full TypeScript support.