kj-model works with text, number, range, textarea, select, checkbox (booleans) and radio buttons (the checked radio's value). It never rewrites the field you are typing in, but programmatic changes — a preset button, a reset — update it immediately.
The mixer below binds the hex field to a getter/setter pair: reading hex computes it from the three channels, writing a valid hex parses it back into them. That is all two-way binding needs.
View the source for this demo
<div kj-data="mixer" class="mx">
<div class="mx-swatch" :style="'background:' + rgb() + ';color:' + ink()" role="img" :aria-label="'Colour ' + hex">
<span kj-text="hex"></span>
</div>
<div class="mx-ctrls">
<div class="field"><label for="mx-r">Red <span kj-text="r"></span></label><input id="mx-r" type="range" min="0" max="255" kj-model="r"></div>
<div class="field"><label for="mx-g">Green <span kj-text="g"></span></label><input id="mx-g" type="range" min="0" max="255" kj-model="g"></div>
<div class="field"><label for="mx-b">Blue <span kj-text="b"></span></label><input id="mx-b" type="range" min="0" max="255" kj-model="b"></div>
<div class="field"><label for="mx-hex">Hex (type a value)</label><input id="mx-hex" type="text" kj-model="hex" spellcheck="false" style="font-family:var(--mono)"></div>
<div class="row mx-presets" aria-label="Presets">
<button type="button" @click="hex = '#4D7CFF'" style="background:#4D7CFF" aria-label="Core blue"></button>
<button type="button" @click="hex = '#FF4F8B'" style="background:#FF4F8B" aria-label="Hyper pink"></button>
<button type="button" @click="hex = '#22CFA2'" style="background:#22CFA2" aria-label="Pulse mint"></button>
<button type="button" @click="hex = '#FFB224'" style="background:#FFB224" aria-label="Motion amber"></button>
</div>
<p aria-live="polite" style="margin:0" kj-text="'White text contrast: ' + contrast() + ':1 ' + (contrast() >= 4.5 ? '✓ passes AA' : '✗ below AA')"></p>
</div>
</div>$.component('mixer', () => ({
r: 108, g: 92, b: 231,
get hex() {
return '#' + [this.r, this.g, this.b].map(n => (+n).toString(16).padStart(2, '0')).join('').toUpperCase();
},
set hex(value) { // typing a valid hex updates all three sliders
const m = /^#?([0-9a-f]{6})$/i.exec(String(value).trim());
if (m) [this.r, this.g, this.b] = [0, 2, 4].map(i => parseInt(m[1].slice(i, i + 2), 16));
},
rgb() { return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; },
luminance() {
return [this.r, this.g, this.b]
.map(v => { v /= 255; return v <= .03928 ? v / 12.92 : Math.pow((v + .055) / 1.055, 2.4); })
.reduce((sum, v, i) => sum + v * [.2126, .7152, .0722][i], 0);
},
contrast() { return +(1.05 / (this.luminance() + .05)).toFixed(2); },
ink() { return this.contrast() >= 4.5 ? '#FFFFFF' : '#13152B'; },
}));More examples
Every control type
<input kj-model="email" type="email">
<input kj-model="qty" type="number"> <!-- string: use +qty for maths -->
<input kj-model="agree" type="checkbox"> <!-- boolean -->
<input kj-model="size" type="radio" value="m"> <input kj-model="size" type="radio" value="l">
<select kj-model="country">…</select>API
Two-way binding for text, number and range inputs, textareas, selects, checkboxes (booleans) and radio buttons (the checked radio’s value). The field being typed in is never rewritten mid-typing; programmatic changes update it immediately.
Reference: kj-model →Migrating
x-model → kj-model. Values arrive as strings (use +value); Alpine's .number, .lazy and .debounce modifiers are not included.Related features
Reactive state
Declare state on an element with kj-data and every binding inside updates when it changes.
Open →PulseAttribute binding
Bind any attribute to state with :attr or kj-bind:attr — src, alt, aria-*, disabled, style.
Open →CoreForms & serialization
Turn forms into query strings or plain objects with serialize() and serializeObject(), then submit them with $.post — or declaratively with kj-post..
Open →