> ## Documentation Index
> Fetch the complete documentation index at: https://aerojs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Reactive bindings

> Wire DOM to <script is:state> with event handlers, text, visibility, classes, properties, and form controls.

## The problem

Reactive UI needs more than text that updates: buttons need click handlers, sections need show/hide, forms need two-way input, and loading states need `disabled` or `aria-busy`. Wiring each concern by hand duplicates selectors and listener cleanup.

## How Aero helps

With `<script is:state>` and `reactivity: true`, bindings connect markup to state declarations. The compiler emits `data-aero-*` attributes and a mount pass that keeps the DOM in sync.

All binding attribute names also accept `aero-*` and `data-aero-*` prefixes (for example `data-aero-on-click`).

## Text interpolation

`{ expression }` in markup becomes a live text binding when a state script is present:

```html theme={"theme":"github-dark-default"}
<script is:state>
	let count = 0
</script>

<p>{ count }</p>
```

For binding text on a void or self-contained element, use the `text` attribute:

```html theme={"theme":"github-dark-default"}
<li for="{ const item of items }" key="{ item.id }" text="{ item.label }"></li>
```

## Event handlers

Use `on:<event>` with an expression body. Modifiers use dot syntax (same idea as Vue):

```html theme={"theme":"github-dark-default"}
<button on:click="{ count++ }">Increment</button>

<form on:submit.prevent="{ save() }">…</form>
```

Event expressions run with state bindings in scope. Assignments like `open = !open` update signals directly.

## Visibility (`show`)

Toggle presence without removing the node from the tree (display toggling):

```html theme={"theme":"github-dark-default"}
<script is:state>
	let open = false
</script>

<button on:click="{ open = !open }">Toggle</button>
<div show="{ open }">Visible when open is true</div>
```

## HTML (`html`)

Set `innerHTML` from a state expression. Use only with trusted content:

```html theme={"theme":"github-dark-default"}
<script is:state>
	let body = '<strong>Updated</strong>'
</script>

<div html="{ body }"></div>
```

## Class toggles (`class:<name>`)

Bind a boolean to a class name:

```html theme={"theme":"github-dark-default"}
<button on:click="{ active = !active }" class:is-active="{ active }">
	Toggle highlight
</button>
```

## Attribute bindings (default)

With `<script is:state>`, `{ stateRef }` on an ordinary attribute keeps the attribute name in the DOM and updates it reactively via `setAttribute` / `removeAttribute`:

```html theme={"theme":"github-dark-default"}
<script is:state>
	let theme = 'system'
	let path = '/dashboard'
</script>

<html data-theme="{ theme }">
	<body>
		<a href="{ path }">Dashboard</a>
	</body>
</html>
```

Boolean flag bindings (`data-*` or any hyphenated name like `is-even`) use presence-only semantics: `true` adds the attribute; `false` removes it. String `data-*` values (like `data-theme`) are emitted as strings. `aria-*` booleans emit `"true"` / `"false"`. Non-hyphenated attrs (like `href`) stringify booleans.

## Property bindings (IDL exceptions)

Bind IDL properties that do not round-trip through content attributes (`disabled`, `readOnly`, `tabIndex`, `hidden`, `indeterminate`):

```html theme={"theme":"github-dark-default"}
<script is:state>
	let saving = false
</script>

<button disabled="{ saving }" on:click="{ saving = !saving }">Save</button>
```

## Form model bindings

Two-way bindings for common form controls:

| Control                      | Attribute | Example                                                |
| ---------------------------- | --------- | ------------------------------------------------------ |
| Text input, textarea, select | `value`   | `value="{ email }"`                                    |
| Checkbox                     | `checked` | `checked="{ agree }"`                                  |
| Radio group                  | `checked` | `checked="{ plan }"` (value from `value` on the input) |

```html theme={"theme":"github-dark-default"}
<script is:state>
	let email = ''
	let agree = false
	let plan = ''
</script>

<input type="email" value="{ email }" />
<input type="checkbox" checked="{ agree }" />
<select value="{ plan }">
	<option value="free">Free</option>
	<option value="pro">Pro</option>
</select>
```

### Nested object fields

Group related form fields in one object binding. Member access in bindings and handlers mutates in place:

```html theme={"theme":"github-dark-default"}
<script is:state>
	let formModel = { email: '', agree: false, plan: '' }

	const isComplete = () =>
		formModel.email.length > 0 && formModel.agree && formModel.plan.length > 0
</script>

<form class:is-valid="{ isComplete() }">
	<input type="email" value="{ formModel.email }" />
	<input type="checkbox" checked="{ formModel.agree }" />
	<select value="{ formModel.plan }">…</select>
</form>
```

`value="{ formModel.email }"` and `checked="{ formModel.agree }"` stay in sync when the user types or when code assigns `formModel.email = '…'`. The runtime skips DOM writes when the control already matches state so the caret stays put during typing.

Reactive form-model and property binds also **mirror native content attributes** in SSR output and at runtime, so `[checked]`, `[value]`, `[disabled]`, and similar attribute selectors keep working. Mount markers (`data-aero-model-*`, `data-aero-property-*`) remain in the HTML for binding wiring.

## Loading state (`busy`)

When **both** `reactivity: true` and `hypermedia: true` are enabled, `busy` ties a boolean state binding to hypermedia request lifecycle on an element:

```html theme={"theme":"github-dark-default"}
<script is:state>
	let isSaving = false
</script>

<button busy="{ isSaving }" on:click="{ POST('/api/save', { state: isSaving }) }">
	Save
</button>
```

`busy` requires a declared boolean `let` binding. Hypermedia sets the signal while the request runs and restores it when the swap settles.

See [Hypermedia recipes](/guide/hypermedia) for action options.

## Side effects (`$effect`)

When declarative binds are not enough — web component `.update()`, imperative DOM, or third-party subscriptions — use top-level `$effect(() => { … })` in `<script is:state>`. See [Reactive effects](/guide/reactivity/effects) for syntax, cleanup, and a numeric-text wrapper example.
