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