| name | form-binding |
| description | Two-way binding for `<input>`, `<textarea>`, `<select>` via writable signals. Load this when the task is about wiring a form field to a signal — text input, checkbox, or select — or doing derived validation that updates as the user types. |
Form Two-Way Binding
For specific properties of <input>, <textarea>, and <select>, passing a writable signal (Signal.State) automatically sets up two-way binding. The DOM event syncs back to the signal.
| Element | Property | Event |
|---|
<input> (text / search / etc) | value | input |
<input type="checkbox"> | checked | change |
<textarea> | value | input |
<select> | selectedIndex | change |
Imports
import { signal } from "@takanashi/rikka-signal";
import { input, textarea, select, option } from "@takanashi/rikka-dom";
Text input
const name = signal("");
input({ value: name });
Textarea
const bio = signal("");
textarea({ value: bio });
Checkbox
const agreed = signal(false);
input({ type: "checkbox", checked: agreed });
Select
<select> uses selectedIndex (the index, not the value), so pair it with an options array or a computed mapping:
const options = ["red", "green", "blue"];
const selectedIndex = signal(0);
select(
{ selectedIndex },
options.map((o) => option({}, o)),
);
The signal's type is number, not the option's value. If you need value-based binding, compute the index from the value:
const color = signal("red");
const options = ["red", "green", "blue"];
const idx = computed(() => options.indexOf(color.get()));
const idxWritable = signal(idx.get());
Reading vs writing the bound signal
Two-way binding means changes flow both ways:
const text = signal("");
input({ value: text });
console.log(text.get());
text.set("hello");
The same signal can drive other parts of the UI:
input({ value: text });
p({}, "You typed: ", text);
Combining with validation
Combine with computed for derived validation:
import { signal, computed } from "@takanashi/rikka-signal";
import { input, p, div, button } from "@takanashi/rikka-dom";
const email = signal("");
const isValid = computed(() => /.+@.+\..+/.test(email.get()));
const canSubmit = computed(() => isValid.get());
div({},
input({ type: "email", value: email }),
p({}, "Status: ", () => isValid.get() ? "✓" : "invalid"),
button({ disabled: computed(() => !canSubmit.get()) }, "Submit"),
);
In defineElement attributes
The same rule does not apply to attributes defined via defineElement. The value / checked / selectedIndex two-way binding is a feature of rikka-dom's input handling, not of defineElement attributes.
For custom elements with form-like behavior, use a declared attributes entry plus an events entry. See ../custom-element/.
Pitfalls
1. Using a computed signal — it's read-only
input({ value: computed(() => "hello") });
const text = signal("");
input({ value: text });
2. Two-way binding only on value / checked / selectedIndex
input({ placeholder: signal("Type here") });
input({ placeholder: "Type here" });
If you need a dynamic placeholder, pass the signal — but it will be one-way (signal → DOM, not the reverse):
const ph = signal("Type here");
input({ placeholder: ph });
3. Forgetting change vs input semantics
rikka auto-picks the right event:
value on text inputs / textarea → input (every keystroke)
checked on checkboxes → change (on toggle)
selectedIndex on selects → change (on selection)
You don't need to add manual event listeners — just declare the property and the binding works.
4. Using value on a <select>
<select>.value is the option's string value, but rikka's two-way binding uses selectedIndex (a number). If your data is value-based, compute the index from the value.
See also