How it works
The kj-data object is wrapped in a Proxy. When a property changes, the component re-evaluates its bindings (kj-text, kj-show, :attr, kj-model). Methods defined on the object can be called from any expression, and this refers to the state.
Nested components get their own scope; an element binds to its nearest kj-data ancestor. If the object has an init() method, it runs once when the component mounts and receives the root element.
Expressions are evaluated with new Function, like Alpine's standard build, so pages using Pulse need 'unsafe-eval' if you run a strict Content-Security-Policy.
View the source for this demo
<div class="tipcalc" kj-data="{
bill: 84, tip: 18, people: 3,
total() { return +this.bill * (1 + this.tip / 100); },
each() { return this.total() / Math.max(1, +this.people); }
}">
<div class="field"><label for="tc-bill">Bill ($)</label><input id="tc-bill" type="number" min="0" step="0.01" kj-model="bill"></div>
<div class="field"><label for="tc-tip">Tip: <span kj-text="tip + '%'"></span></label><input id="tc-tip" type="range" min="0" max="30" kj-model="tip"></div>
<div class="field"><span style="font-weight:600;font-size:14.5px">People</span>
<div class="row">
<button type="button" @click="people = Math.max(1, +people - 1)" aria-label="Fewer people">−</button>
<output kj-text="people" aria-live="polite"></output>
<button type="button" @click="people = +people + 1" aria-label="More people">+</button>
</div></div>
<div class="tc-result" aria-live="polite"><span>Each pays</span><strong kj-text="'$' + each().toFixed(2)"></strong><small kj-text="'Total $' + total().toFixed(2) + ' incl. tip'"></small></div>
</div>More examples
State, methods and init()
<div kj-data="{ open: false, items: [], async init() { this.items = await $.getJSON('/api/items'); } }">
<button @click="open = !open" :aria-expanded="open">Menu</button>
<ul kj-show="open" kj-html="items.map(i => '<li>' + $.escape(i.name) + '</li>').join('')"></ul>
</div>API
Declares a component and its reactive scope. An init() method, if present, runs on mount. Pass a registered component name to reuse logic.
A standalone reactive proxy without auto-rendering — for your own change-tracking logic.
Reference: $.reactive() →Migrating
x-data → kj-data, same object syntax and init(). Alpine's magic properties ($watch, $dispatch, $refs, $nextTick) are not included; $el and $event are.Related features
Two-way binding
Bind form controls to state in both directions with kj-model — text, number, range, checkbox, radio and select.
Open →PulseText & HTML binding
Bind element content to state with kj-text (always safe) and kj-html (for markup you control).
Open →PulseReusable components
Register a component once with $.component(name, factory) and reuse it anywhere with kj-data="name(args)".
Open →