Skip to main content
Aero is static-first: build-time { } 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:
Build script runs once at compile (or request) time. Owned 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-level let 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 or innerHTML), process bindings into the page store:
See Process runtime fragments.

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.