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:

source
<script>
let name = ''
</script>

<input bind:value={name} placeholder="Type your name" />

<p>Hello, {name}!</p>
output

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:

source
<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>
output

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.

source
<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>
output

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>
output
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>

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>
output
1

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>
output

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 own let and assign it back yourself.
  • bind: and inline event handlers written inside a component's slot content are dropped, and bind: 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:open only; any other bind: 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:

source
<!-- 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