⬡ FRAMEWERK
Core

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.

View as Markdown

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:

Navigate and toggle
phone
SPEC.JSON
RENDERED OUTPUTFRAMEWERK RENDERER
Oat milk
Order
Edit the JSON to update the rendered screen.Edits stay in this preview

A conditional action — first match wins, unconditional branch last:

Conditional continue
phone
SPEC.JSON
RENDERED OUTPUTFRAMEWERK RENDERER
Checkout
I agree
Continue
Edit the JSON to update the rendered screen.Edits stay in this preview

Anatomy

Ops are separated by ; or |.

ActionEffect
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.

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 stateVar the 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 own setState, so setState(selected, sarah) | navigate(detail, athlete=$state.selected) passes sarah.
  • $self.value is 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:

PropRuns
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.
hoverActionWhen the pointer enters the node.
leaveActionWhen 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() and back() need a host. State actions (setState, toggleState) work out of the box, but the renderer does not own a screen stack — it reports navigation through onNav/onBack callbacks 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.