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 needdisabled 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:
text attribute:
Event handlers
Useon:<event> with an expression body. Modifiers use dot syntax (same idea as Vue):
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:
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.