Skip to main content

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:
For binding text on a void or self-contained element, use the text attribute:

Event handlers

Use on:<event> with an expression body. Modifiers use dot syntax (same idea as Vue):
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 (html)

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

Class toggles (class:<name>)

Bind a boolean to a class name:

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:
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):

Form model bindings

Two-way bindings for common form controls:

Nested object fields

Group related form fields in one object binding. Member access in bindings and handlers mutates in place:
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:
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 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 for syntax, cleanup, and a numeric-text wrapper example.