Actions
The action prop makes a node interactive; the grammar is six verbs — navigate, back, setState, toggleState, scrollTo, openUrl — plus conditional branches; onEnter runs one after a wait, and hoverAction/leaveAction when the pointer enters or leaves.
Actions
The action prop makes a node interactive; everything interactive is a string — there is no embedded JavaScript and no expression compiler, and the action grammar is six verbs.
Examples
Navigation and state changes:
A conditional action — first match wins, unconditional branch last:
Anatomy
Ops are separated by ; or |.
| Action | Effect |
|---|---|
navigate(screenId) | Go to a screen. |
navigate(screenId, key=value) | Go to a screen, which arrives with key holding value. |
back() | Pop the back stack. |
setState(key, value) | Set a state variable. |
toggleState(key) | Flip a boolean. |
scrollTo(anchor) | Jump to the node on the same screen that carries anchor: "name". |
openUrl(address) | Open a web address in a new tab: https://, http://, mailto:, tel:, a path, or a #fragment. Anything else is refused. |
{ "type": "button", "content": "Order", "action": "navigate(order)" }
{ "type": "button", "content": "Oat milk", "action": "toggleState(oatMilk)" }
{ "type": "button", "content": "Reset", "action": "setState(tab, home); setState(filter, all)" }
A key in the app. namespace writes app state, which a navigating host keeps when it moves to the next screen: setState(app.lastAdded, cappuccino) | navigate(menu). The name must be declared on project.stateVars.
Navigate arguments
A list of eleven products has one product screen. Each card says which product it opens:
{ "type": "pressable", "action": "navigate(product, piece=low-tide-cup)" }
The arguments are the state the target screen arrives with: over its declared defaults and the persona, under anything pressed after arrival. They belong to the visit, so back() returns to a screen with the arguments it was opened with.
- Each key names a
stateVarthe target screen declares. An argument it does not declare is a validation warning that lists what it does. - A value is a literal or
$state.<name>.$state.<name>is read after the press's ownsetState, sosetState(selected, sarah) | navigate(detail, athlete=$state.selected)passessarah. $self.valueis refused: a press holds no value of its own. Bind the control to a variable and pass that variable.- A malformed argument refuses the whole
navigate; a navigate that arrived with two of its three arguments would open the wrong record and say nothing. - Inside a component, the action reads the instance's prop:
"navigate(product, piece={piece})".
An argument is read by one screen for one visit. A value several screens read is app state. The host receives the arguments, resolved, as the second parameter of onNav.
A legacy colon syntax — set:key=value and toggle:key — is still parsed and will keep working; prefer the function forms in new specs.
Conditional actions take an array of branches, evaluated top to bottom, first match wins. A branch with no if is an unconditional fallback — put it last.
Segments matching no verb are collected as invalid and reported by the validator rather than silently doing nothing — a typo'd action is a loud failure, not a dead button.
Actions that run without a press
action runs on a click. Three more props take the same action strings and run them at another moment:
| Prop | Runs |
|---|---|
onEnter: { after, do } | On a screen, each time the screen is arrived on; on a node, each time the node appears. after is a wait in milliseconds (0 when absent); do is the action string. Leaving first cancels it. |
hoverAction | When the pointer enters the node. |
leaveAction | When the pointer leaves the node. |
{ "id": "splash", "onEnter": { "after": 800, "do": "navigate(home)" } }
{ "type": "row", "_id": "nav-shop",
"hoverAction": "setState(shopMenu, open)",
"leaveAction": "setState(shopMenu, closed)" }
A splash that moves on, an onboarding step that advances itself and a toast that leaves on its own are onEnter. A menu that opens on hover is state the showWhen beneath it already reads.
All three run only where a person plays the prototype. A screenshot, a thumbnail or a filmstrip never starts a timer and never hovers, so a capture of a splash shows the splash. A phone or a tablet has no hover: a node that opens something on hover on a touch device needs a tap that reaches it too (its own action, or a pressable around it), and validation names one that has none. An onEnter without do, or with a wait that is not a number of milliseconds, is an error; its do is checked exactly as action is.
navigate()andback()need a host. State actions (setState,toggleState) work out of the box, but the renderer does not own a screen stack — it reports navigation throughonNav/onBackcallbacks and the host decides what happens. A host that passes no-ops gets a screen that renders correctly and never navigates. See the renderer README.
Stability
The six-verb grammar, navigate's key=value arguments, the conditional-array form, onEnter, hoverAction, leaveAction, and the legacy colon syntax are part of the format contract. Adding a verb is additive; removing one, or changing how an existing verb parses, is breaking. Invalid segments are warnings, not errors — treat warnings as failures in CI.