{ } interpolation renders once. When a page needs counters, toggles, form state, or UI that updates after load, enable reactivity. You keep writing .html with a fifth script type — no separate client framework required.
Reactivity defaults to
false. Static sites ship no client binding runtime until you opt in.Enable reactivity
aero.config.ts
aero check reports templates that use reactive directives when the flag is off.
<script is:state>
State scripts run in the browser after mount. Top-level let declarations become signals wired to markup:
See Scripts for how state scripts fit the script taxonomy.
Seeding state from is:build
At render time, Aero runs <script is:build> then <script is:state> in the same scope. Declare data in the build script and assign it to state bindings:
let values are serialized into a <script type="application/json" data-aero="state"> hydration payload so the client store matches the HTML on first paint. Derived bindings (for example let total = items.length) are recomputed at mount — they are not included in the payload.
Common sources for seed data:
Build-time
{ expression } in markup still renders static HTML. Use is:state when that data should stay reactive after load.
Collection state (objects, arrays, Map, Set)
Top-levellet bindings can hold collection values. Aero wraps them in a coarse-grained reactive proxy: any nested mutation re-notifies effects that read that binding.
Replacing the whole binding (
items = next) still works and is a good fit when you want immutable list updates (for example with view transitions). In-place mutation is supported for ergonomics; both paths notify the same top-level signal.
Granularity: v1 is binding-level, not per-property. Reading items.length in a derived binding (let hasItems = items.length > 0) re-runs when items notifies — including after push, not only after items = ….
Hydration: Owned collection values serialize into <script type="application/json" data-aero="state">. Plain objects and arrays round-trip as JSON; Map and Set use __aero markers in the payload.
Non-reactive values: Primitives, functions, Date, class instances, DOM nodes, and typed arrays are stored as-is — mutating a Date in place does not notify.
Helpers and handlers
Declare helpers at the top level of<script is:state> with normal JavaScript — block or expression bodies, TypeScript parameter types, and mixed styles in one script:
function foo() and const foo = () => … behave the same for templates and event handlers. Pure helpers that never touch reactive let bindings (for example createID above) stay at module scope; helpers that read or write state are available to bindings automatically.
What you can bind
With reactivity enabled, markup can update at runtime:
Attribute names also accept
aero-* and data-aero-* prefixes.
Hydration
Server-rendered HTML is the first paint. Aero emits a<script type="application/json" data-aero="state"> JSON payload with owned let binding values from render time (including values seeded from is:build). At mount, the client store reads that payload and attaches bindings — no manual hydration API.
Runtime access
For imperative inserts (hypermedia swaps orinnerHTML), process bindings into the page store:
Hypermedia
Reactivity alone covers client-only UI. Pair it with Hypermedia when fragments come from the server.busy and post-swap binding processing require both flags.
Next steps
Reactivity recipes
Common patterns: forms, lists, reactive props, when to use Alpine instead.
Bindings reference
Full syntax for events, show, html, class, property, and model bindings.
Reactive effects
Imperative side effects with
$effect — web components, subscriptions, cleanup.Structural updates
Reactive
if, keyed for, and switch.Reactive props
Share mutable state between parent and child components.