Skip to content

The power of a modern framework, in a runtime the browser barely notices.

Your server keeps rendering HTML. sigmx fetches it, morphs it into the page without losing your place, and adds signals, two-way binding and streaming. The build ships only the plugins your markup uses. Zero dependencies.

4.5 KB core·50 plugins·0 dependencies·MIT

The markup
<div data-signals="{ count: 0 }" class="row">
<button data-on:click="$count++">Clicked <b data-text="$count"></b> times</button>
<button data-on:click="@get('/api/time')">Ask the server</button>
<span id="server-time" class="muted">no reply yet</span>
</div>
In the browser
no reply yet

Signals update in place. The second button asks this site's server for HTML.

On the wire
GET /api/time
Accept: text/event-stream, text/html, application/json

200 OK
Content-Type: text/html

…

Same id as the page, so it is morphed in. JSON would merge into signals. An event stream keeps delivering.

Every kilobyte, counted.

One tile is 512 bytes, brotli-compressed the way a CDN serves it. The core is the dark tiles; everything after it is plugins, and you only ship the ones you register. All 50 of them together, including the Alpine and htmx extensions, come to 21% less than htmx alone and 2% smaller than Datastar.

sigmx coresignals, store, expressions, the engine4.5 KB
sigmx minimalcore + signals, text, show, on5.1 KB
a typical sigmx pageminimal + fetch + morph8.2 KB
sigmx essentialsstate, rendering, forms, requests, morph9.0 KB
sigmx, everythingall 50 plugins in one file11.5 KB
Datastar 1.0.3datastar.js11.8 KB
htmx 2.0.10htmx.min.js, core only14.6 KB
Alpine.js 3.17.1cdn.min.js, core only17.6 KB

corepluginsother libraries, measured the same way

Measured 2026-09-06: htmx 2.0.10 (htmx.min.js, core only), Datastar 1.0.3 (datastar.js), Alpine.js 3.17.1 (cdn.min.js, core only), with npm run size and npm run compare at brotli quality 11. Per-plugin figures and every preset are in bundle sizes.

One attribute, read left to right.

Everything the runtime does is declared in markup, and every part of an attribute means something. The browser and the build-time scanner read the same six pieces.

<input data-bind="q" data-on:input__debounce.300ms="@get('/search?q=' + $q)">
  1. 1
    data- prefixThe attribute prefix. Configurable per project; hx- and x- style prefixes work too.
  2. 2
    on pluginThe plugin that owns this attribute, 472 bytes, registered because this line uses it.
  3. 3
    :input keyThe key: here an event name. For data-bind it names a signal, for data-class a class.
  4. 4
    __debounce.300ms modifiersModifiers with arguments: debounce, throttle, prevent, once, window, outside and more.
  5. 5
    @get actionAn action from a plugin. The HTML that comes back is morphed into the page by id.
  6. 6
    $q signalA signal, two-way bound to the input by data-bind and read like any variable.

You write the markup. The scanner writes the bundle.

Auto mode reads your source the way a CSS framework scans for class names. Every attribute and action it finds drops its plugin into the bundle, along with the plugins that one needs; the other42 never leave the bundler. Nothing to maintain, nothing to forget, nothing shipped by accident.

src/pages/subscribe.html
<form data-signals="{ open: false, sent: false }"      data-on:submit__prevent="@post('/subscribe')">  <button type="button" data-on:click="$open = !$open"          data-text="$open ? 'Close' : 'Subscribe'"></button>  <fieldset data-show="$open">    <input name="email" data-on:input__debounce.300ms="@get('/check')">    <p id="check"></p>    <button>Send</button>  </fieldset>  <p data-show="$sent">Thanks, check your inbox.</p></form>
dist/app.js what ships

8.2 KB brotli, measured for exactly these 8 plugins

data-signals142 B, line 1
data-on472 B, line 2
@post1592 B, line 2
data-text117 B, line 4
data-show135 B, line 5
@get1626 B, line 6
patch-elements1345 B, needed by @get
patch-signals67 B, needed by @get
core4.5 KB, signals, store, expressions, the engine

42 plugins never entered the file.

Brick widths are each plugin measured on its own; together they compress to the total on top. The same scanner runs as a CLI for any build, a Vite plugin for any framework, an Astro integration, and inside the Rust crate for projects without Node. Turn on precompilation as well and every expression becomes a real function in a table, so the runtime compiler leaves the bundle and a strict Content-Security-Policy needs no exceptions. How auto mode works.

Every built-in is twenty lines you could have written.

Three helpers make every directive, function and server-event handler in the library, and they are the same three you get. Nothing registers on import, so your plugins tree-shake, teardown is automatic, and auto mode scans them like the built-ins.

  • attribute()a directivedata-upper, data-clock, data-persist
  • action()a function in expressions@confirm, @clipboard, @share
  • handler()a server eventpatch-elements, patch-signals, toast
  • Extending walks through all three and ends with a complete Plausible analytics plugin in one small file.
upper.ts
import { attribute } from 'sigmx'
// <h1 data-upper="$title"></h1>
export const upper = attribute({
name: 'upper',
value: 'required',
mount({ el, evaluate, effect }) {
effect(() => { el.textContent = String(evaluate()).toUpperCase() })
},
})

Three libraries made this one possible.

sigmx is not a new idea. It is a small, independent implementation of ideas three libraries proved first, and it would not exist without them. If you already use one, you will feel at home.

htmx

Showed that the browser already is a hypermedia client, and only needed a few more verbs. Any element can make a request, the server answers with HTML, and the page swaps it in. Server-sent events carry updates.

  • what sigmx keeps
  • the server sends HTML
  • boosted links and forms
  • event streams for updates

Alpine.js

Showed that state and behaviour belong next to the markup they drive. x-data, x-model and x-show made reactive attributes feel like HTML, with expressions that are simply JavaScript.

  • what sigmx keeps
  • state declared in attributes
  • expressions as plain JavaScript
  • the attribute style itself

Datastar

Showed that the two ideas are one. Signals in the browser and patches from the server share a single attribute vocabulary, and the server can drive the page as easily as the page drives itself.

  • what sigmx keeps
  • the attribute vocabulary
  • signals and server patches together
  • markup written for it runs unchanged

What is different is the implementation: no shared code, a smaller and simpler codebase, plugins you register rather than side effects that register themselves, and a build that does the heavy lifting. The introduction tells the full story.

A script tag, or an npm install.