Socket
Book a DemoInstallSign in
Socket

sprae

Package Overview
Dependencies
Maintainers
2
Versions
146
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

sprae

DOM microhydration

11.6.0
latest
Source
npmnpm
Version published
Weekly downloads
70
141.38%
Maintainers
2
Weekly downloads
 
Created
Source

∴ spræ tests npm bundle size npm

DOM tree microhydration

Sprae is open & minimalistic progressive enhancement framework with preact-signals reactivity.
Useful for SPAs, PWAs, lightweight UI or nextjs / SSR (see JSX).
An alternative to alpine, petite-vue, lucia etc (see why).

Usage

<div id="container" :if="user">
  Hello <span :text="user.name">there</span>.
</div>

<script type="module">
  import sprae from './sprae.js' // https://unpkg.com/sprae/dist/sprae.min.js

  // init
  const container = document.querySelector('#container');
  const state = sprae(container, { user: { name: 'friend' } })

  // update
  state.user.name = 'love'
</script>

Sprae evaluates :-directives and evaporates them, returning reactive state for updates.

UMD

sprae.umd enables sprae via CDN, CJS, AMD etc.

<script src="https://unpkg.com/sprae/dist/sprae.umd"></script>
<script>
  window.sprae; // global standalone
</script>

Autoinit

sprae.auto autoinits sprae on document body.

<!-- Optional attr `prefix` (by default ':'). -->
<script src="https://unpkg.com/sprae/dist/sprae.auto" prefix="js-"></script>

Directives

:if="condition", :else

Control flow of elements.

<span :if="foo">foo</span>
<span :else :if="bar">bar</span>
<span :else>baz</span>

<!-- fragment -->
<template :if="foo">foo <span>bar</span> baz</template>

:each="item, index? in items"

Multiply element.

<ul><li :each="item in items" :text="item" /></ul>

<!-- cases -->
<li :each="item, idx in array" />
<li :each="value, key in object" />
<li :each="count, idx in number" />

<!-- fragment -->
<template :each="item in items">
  <dt :text="item.term"/>
  <dd :text="item.definition"/>
</template>

:text="value"

Set text content of an element.

Welcome, <span :text="user.name">Guest</span>.

<!-- fragment -->
Welcome, <template :text="user.name"><template>.

:class="value"

Set class value.

<div :class="foo"></div>

<!-- appends to static class -->
<div class="bar" :class="baz"></div>

<!-- array/object, a-la clsx -->
<div :class="['foo', bar && 'bar', { baz }]"></div>

:style="value"

Set style value.

<span style="'display: inline-block'"></span>

<!-- extends static style -->
<div style="foo: bar" :style="'bar-baz: qux'">

<!-- object -->
<div :style="{barBaz: 'qux'}"></div>

<!-- set CSS variable -->
<div :style="{'--bar-baz': qux}"></div>

:value="value"

Set value to/from an input, textarea or select (like alpinejs x-model).

<input :value="value" />
<textarea :value="value" />

<!-- selects right option & handles selected attr -->
<select :value="selected">
  <option :each="i in 5" :value="i" :text="i"></option>
</select>

<!-- handles checked attr -->
<input type="checkbox" :value="item.done" />

:<prop>="value", :="values"

Set any attribute(s).

<label :for="name" :text="name" />

<!-- multiple attributes -->
<input :id:name="name" />

<!-- spread attributes -->
<input :="{ id: name, name, type: 'text', value }" />

:with="values"

Define values for a subtree.

<x :with="{ foo: 'bar' }">
  <y :with="{ baz: 'qux' }" :text="foo + baz"></y>
</x>

:fx="code"

Run effect, not changing any attribute.

<div :fx="a.value ? foo() : bar()" />

<!-- cleanup function -->
<div :fx="id = setInterval(tick, 1000), () => clearInterval(id)" />

:ref="name", :ref="el => (...)"

Expose element in state with name or get reference to element.

<div :ref="card" :fx="handle(card)"></div>

<!-- local reference -->
<li :each="item in items" :ref="li">
  <input :onfocus..onblur="e => (li.classList.add('editing'), e => li.classList.remove('editing'))"/>
</li>

<!-- set innerHTML -->
<div :ref="el => el.innerHTML = '...'"></div>

<!-- mount / unmount -->
<textarea :ref="el => (/* onmount */, () => (/* onunmount */))" :if="show"></textarea>

:on<event>="handler", :on<in>..on<out>="handler"

Attach event(s) listener with optional modifiers.

<input type="checkbox" :onchange="e => isChecked = e.target.value">

<!-- multiple events -->
<input :value="text" :oninput:onchange="e => text = e.target.value">

<!-- sequence of events -->
<button :onfocus..onblur="e => (handleFocus(), e => handleBlur())">

<!-- modifiers -->
<button :onclick.throttle-500="handler">Not too often</button>
Modifiers:
  • .once, .passive, .capture – listener options.
  • .prevent, .stop (.immediate) – prevent default or stop (immediate) propagation.
  • .window, .document, .parent, .outside, .self – specify event target.
  • .throttle-<ms>, .debounce-<ms> – defer function call with one of the methods.
  • .<key> – filtered by event.key:
    • .ctrl, .shift, .alt, .meta, .enter, .esc, .tab, .space – direct key
    • .delete – delete or backspace
    • .arrow – up, right, down or left arrow
    • .digit – 0-9
    • .letter – A-Z, a-z or any unicode letter
    • .char – any non-space character
    • .ctrl-<key>, .alt-<key>, .meta-<key>, .shift-<key> – key combinations, eg. .ctrl-alt-delete or .meta-x.
  • .* – any other modifier has no effect, but allows binding multiple handlers to same event (like jQuery event classes).

:data="values"

Set data-* attributes. CamelCase is converted to dash-case.

<input :data="{foo: 1, barBaz: true}" />
<!-- <input data-foo="1" data-bar-baz /> -->

:aria="values"

Set aria-* attributes. Boolean values are stringified.

<input role="combobox" :aria="{
  controls: 'joketypes',
  autocomplete: 'list',
  expanded: false,
  activeOption: 'item1',
  activedescendant: ''
}" />
<!--
<input role="combobox" aria-controls="joketypes" aria-autocomplete="list" aria-expanded="false" aria-active-option="item1" aria-activedescendant>
-->

Signals

Sprae uses preact-flavored signals for reactivity and can take signal values as inputs.
Signals can be switched to an alternative preact/compatible implementation:

import sprae from 'sprae';
import { signal, computed, effect, batch, untracked } from 'sprae/signal';
import * as signals from '@preact/signals-core';

// switch sprae signals to @preact/signals-core
sprae.use(signals);

// use signal as state value
const name = signal('Kitty')
sprae(el, { name });

// update state
name.value = 'Dolly';
ProviderSizeFeature
ulive350bMinimal implementation, basic performance, good for small states.
@webreflection/signal531bClass-based, better performance, good for small-medium states.
usignal850bClass-based with optimizations, good for medium states.
@preact/signals-core1.47kbBest performance, good for any states, industry standard.
signal-polyfill2.5kbProposal signals. Use via adapter.

Evaluator

Expressions use new Function as default evaluator, which is fast & compact way, but violates "unsafe-eval" CSP. To make eval stricter & safer, as well as sandbox expressions, an alternative evaluator can be used, eg. justin:

import sprae from 'sprae'
import justin from 'subscript/justin'

sprae.use({compile: justin}) // set up justin as default compiler

Justin is minimal JS subset that avoids "unsafe-eval" CSP and provides sandboxing.

Operators:

++ -- ! - + * / % ** && || ??
= < <= > >= == != === !==
<< >> >>> & ^ | ~ ?: . ?. [] ()=>{} in
= += -= *= /= %= **= &&= ||= ??= ... ,

Primitives:

[] {} "" ''
1 2.34 -5e6 0x7a
true false null undefined NaN

Custom Build

Sprae can be tailored to project needs via sprae/core:

// sprae.custom.js
import sprae, { dir, parse } from 'sprae/core'
import * as signals from '@preact/signals'
import compile from 'subscript'

// standard directives
import 'sprae/directive/default.js'
import 'sprae/directive/if.js'
import 'sprae/directive/text.js'

// custom directive :id="expression"
dir('id', (el, state, expr) => {
  // ...init
  return value => el.id = value // update
})

sprae.use({
  // configure signals
  ...signals,

  // configure compiler
  compile,

  // custom prefix, default is `:`
  prefix: 'js-'
})

JSX

Sprae works with JSX via custom prefix.

Case: keep nextjs server components intact by offloading dynamic UI logic (active nav, tabs etc) to sprae instead of client components:

// app/page.jsx - server component
export default function Page() {
  return <>
    <nav id="nav">
      <a href="/" js-class="location.pathname === '/' && 'active'">Home</a>
      <a href="/about" js-class="location.pathname === '/about' && 'active'">About</a>
    </nav>
    ...
  </>
}
// layout.jsx
import Script from 'next/script'

export default function Layout({ children }) {
  return <>
    {children}
    <Script src="https://unpkg.com/sprae" prefix="js-" />
  </>
}

Hints

  • To prevent FOUC add <style>[\:each],[\:if],[\:else] {visibility: hidden}</style>.
  • Attributes order matters, eg. <li :each="el in els" :text="el.name"></li> is not the same as <li :text="el.name" :each="el in els"></li>.
  • Invalid self-closing tags like <a :text="item" /> will cause error. Valid self-closing tags are: li, p, dt, dd, option, tr, td, th, input, img, br.
  • Properties prefixed with _ are untracked: let state = sprae(el, {_x:2}); state._x++; // no effect.
  • To destroy state and detach sprae handlers, call element[Symbol.dispose]().
  • State getters/setters work as computed effects, eg. sprae(el, { x:1, get x2(){ return this.x * 2} }).
  • this is not used, to get current element use :ref.
  • event is not used, :on* attributes expect a function with event argument :onevt="event => handle()", see #46.
  • key is not used, :each uses direct list mapping instead of DOM diffing.
  • await is not supported in attributes, it’s a strong indicator you need to put these methods into state.
  • :ref comes after :if for mount/unmount events <div :if="cond" :ref="(init(), ()=>dispose())"></div>.

Justification

Modern frontend stack is complex and obese, like overprocessed non-organic food. There are healthy alternatives:

Sprae holds open & minimalistic philosophy:

  • Minimal syntax :.
  • Signals for reactivity.
  • Pluggable directives, configurable internals.
  • Small, safe & performant.
  • Bits of organic sugar.
  • Aims at making developers happy 🫰

Examples

🕉

Keywords

hydration

FAQs

Package last updated on 07 Aug 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

About

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.

  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc

U.S. Patent No. 12,346,443 & 12,314,394. Other pending.