Bind Directive
bind: keeps a variable and a form control in step. Type in the
input and the variable changes; change the variable and the input follows.
It is compile-time sugar for an attribute plus an event handler, so there is no watcher and no debounce - the echo lands in the same frame as the keystroke.
Text inputs
Bind a text input with bind:value:
<script>
let name = ''
</script>
<input bind:value={name} placeholder="Type your name" />
<p>Hello, {name}!</p>
Hello, !
When the variable already has the property's name, drop the value:
<input bind:value /> is the same as
bind:value={value}.
Checkboxes
A checkbox binds its checked state, not its value, so it uses
bind:checked:
<script>
let agreed = false
</script>
<label>
<input type="checkbox" bind:checked={agreed} />
I agree to the terms
</label>
<p>{agreed ? 'Thanks - you can continue.' : 'Tick the box to continue.'}</p>
Tick the box to continue.
Selects and textareas
A <select> binds the selected option's value.
A <textarea> works exactly like a text input.
A <select multiple> binds an array of
the selected option values, in document order - so start it as
let picked = [], not ''.
Assigning an array selects exactly those options; assigning a plain value
selects the options carrying it, and null,
undefined or false selects nothing.
<script>
let country = 'us'
</script>
<select bind:value={country}>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
</select>
<p>Selected: {country}</p>
Selected: us
Radio groups
A radio group has one answer, so bind one variable to it with
bind:value.
The radio whose own value matches the variable is the checked
one, and picking another puts that radio's value in the variable.
bind:checked on radios works too - one boolean per radio, and
the radio you pick clears the others in the same name group. It
needs the whole group in one template with a literal name,
which is what lets the compiler pair them up; a group split across
components or built in a loop should use the group value above.
Values arrive as strings, with one exception: on
type="number" and type="range" inputs JAF gives
you a number to do arithmetic with.
An empty number box binds
null, never 0 (half-typed values like
- count as empty too), so the field can always be cleared -
read it as count ?? 0 where your arithmetic needs a number.
Sliders have no empty state, so type="range" is always a
number.
Open state
bind:open binds the open attribute of a
<details> element, so the disclosure state is a variable
you can read and set:
<script>
let expanded = false
</script>
<details bind:open={expanded}>
<summary>Shipping details</summary>
<p>Ships in 2-3 working days.</p>
</details>
<p>The panel is {expanded ? 'open' : 'closed'}.</p>
<style>
details {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
padding: 0.6rem 0.9rem;
}
summary {
cursor: pointer;
font-weight: 500;
}
details p {
margin: 0.5rem 0 0;
color: var(--text-secondary);
}
</style>Shipping details
Ships in 2-3 working days.
The panel is closed.
bind:open on a <dialog> does not
make a modal. Only showModal() puts a dialog in the
browser's top layer with a backdrop; the open attribute shows
a non-modal dialog sitting in normal flow. For real modals use the
built-in overlays - see
Overlays & UI state.
Binding to components
Props flow one way: parent to child.
bind: on a component makes one of them flow back too.
The child declares a plain reactive variable and changes it however it likes:
<!-- components/Stepper.html -->
<script>
let value = 0
</script>
<div class="stepper">
<button onclick="value = value - 1">-</button>
<span>{value}</span>
<button onclick="value = value + 1">+</button>
</div>
<style>
.stepper {
display: inline-flex;
align-items: center;
gap: 0.75rem;
}
.stepper button {
width: 2rem;
height: 2rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
color: var(--text-primary);
cursor: pointer;
}
.stepper span {
min-width: 2ch;
text-align: center;
font-variant-numeric: tabular-nums;
}
</style>The parent binds its own variable to that prop by name:
<!-- pages/order.html -->
<script>
let quantity = 1
</script>
<Stepper bind:value={quantity} />
<p>Ordering {quantity} items.</p>
<button class="reset" onclick="quantity = 10">Set quantity to 10</button>
<style>
.reset {
padding: 0.35rem 0.8rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
color: var(--text-primary);
cursor: pointer;
}
</style>Ordering 1 items.
bind:value={quantity} keeps the two in step in both
directions: the child's buttons update quantity, and anything
that changes quantity updates the child.
Without bind:, value={quantity} still
passes the value down, but the child's changes stay in the child.
Built-in overlays
This is the case most readers hit first: <Modal>,
<Drawer> and <Popover> accept
bind:open.
<script>
let showSettings = false
</script>
<button class="open" onclick="showSettings = true">Settings</button>
<p>showSettings is {showSettings ? 'true' : 'false'}.</p>
<Modal bind:open={showSettings} label="Settings">
<h3>Settings</h3>
<p>Press Escape or click the backdrop to close.</p>
</Modal>
<style>
.open {
padding: 0.35rem 0.8rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
color: var(--text-primary);
cursor: pointer;
}
</style>showSettings is false.
The binding is two-way, so closing the modal with Escape or a backdrop click
sets showSettings back to false for you.
Rules worth knowing
-
The tag must be PascalCase.
<Modal bind:open={x}>binds;<modal bind:open={x}>still resolves to the same component file, but the binding is dropped and the compiler warns, naming the capital you are missing. -
The value must be a bare variable name. A member path such as
bind:open={ui.modal}is passed as a one-way prop instead, with a compile-time warning: the binding syncs the two components' state by name, so it has nowhere to write a path back to. To get two-way sync, lift the value into its ownletand assign it back yourself. -
bind:and inline event handlers written inside a component's slot content are dropped, andbind:warns when it is. Slot content is mounted by the component it is projected into, so the variable would resolve against that component rather than against your template. The component's own opening tag is where your bindings belong, and controls that change your variables belong outside it. - Binding a name the parent never declared warns at mount and binds nothing, rather than blanking the child's own default.
-
bind:on a JSX component is a build error - there is no JAF state on the other side to sync with. Pass a prop in and take the change back out through a callback. -
The built-in overlays honour
bind:openonly; any otherbind:on them is ignored.
Need to share state between files that are not parent and child? That is
unite().
How it works
On a native element, bind: expands to an attribute and a handler:
<!-- This: -->
<input bind:value={name} />
<!-- Becomes: -->
<input value="{name}" oninput="name = event.target.value" />
| Directive | Element | Transforms to |
|---|---|---|
bind:value={x} |
input, textarea, select | value="{x}" oninput="x = event.target.value" |
bind:value={x} |
select[multiple] | oninput="x = Array.from(event.target.selectedOptions, o => o.value)" plus option-by-option write-back |
bind:value={x} |
input[type=radio] | onchange="x = event.target.value" and the radio whose value equals x is checked |
bind:value={x} |
input[type=number] | value="{x}" oninput="x = Number.isNaN(event.target.valueAsNumber) ? null : event.target.valueAsNumber" |
bind:value={x} |
input[type=range] | value="{x}" oninput="x = Number(event.target.value)" |
bind:checked={x} |
checkbox | checked="{x}" onchange="x = event.target.checked" |
bind:open={x} |
details, dialog | open="{x}" ontoggle="x = event.target.open" |
bind:prop={x} |
component (PascalCase) |
__bind_prop="x" - the runtime syncs the parent's
x and the child's prop in both directions
|