Skip to content

DSL: Attributes & events

<article
phaze:class={isFav() ? 'card fav' : 'card'}
phaze:aria-label={`Card for ${name()}`}
phaze:disabled={qty() <= 0}
/>
Transformed via phaze-compiler
<article
class={() => isFav() ? 'card fav' : 'card'}
aria-label={() => `Card for ${name()}`}
disabled={() => qty() <= 0}
/>

The runtime detects the function value and wires a reactive binding. When tracked signals change, the binding re-runs the thunk and writes the result.

phaze: is only available on attributes — for reactive children, use phaze(). The two grammatical positions cover all reactive-expression cases:

<input phaze:value={name()} /> {/* attribute */}
<p>{phaze(`Hello, ${name()}!`)}</p> {/* child */}

on:click is transformed by the phaze-compiler to onClick. The convention is sugar — Phaze’s JSX runtime accepts both forms — but on: matches Solid/Vue and reads more clearly when you also have phaze: and use: on the same element.

<button on:click={fn}>Send</button>
Transformed via phaze-compiler
<button onClick={fn}>Send</button>

Bare call expressions as handler values are auto-thunked at compile time, so the () => ceremony disappears at every event-handler call site:

<button on:click={step.set('email')}>Email</button>
Transformed via phaze-compiler
<button onClick={() => step.set('email')}>Email</button>

The rule is shape-gated: only CallExpression values are wrapped. Function references, arrows, and method references pass through unchanged:

<button on:click={resend}></button> {/* Identifier → pass through */}
<button on:click={(e) => doVerify(e)}></button> {/* Arrow → pass through */}
<button on:click={obj.method}></button> {/* MemberExpression → pass through */}
<button on:click={step.set('email')}></button> {/* CallExpression → auto-thunked */}

Consistent with c / watch / phaze auto-thunking and use:spring’s auto-fuse: phaze-compile writes the boilerplate that’s identical at every call site.

A fuller example — hover + click on a state-machine signal

Section titled “A fuller example — hover + click on a state-machine signal”
import { s } from '@madenowhere/phaze/match'
import { spring } from '@madenowhere/photon/phaze' // use:spring
export default function HoverableDot() {
const state = s<'rest' | 'hover' | 'pressed'>('rest')
const CARD_ANIM = {
rest: { y: 0, opacity: 1 },
hover: { y: -4, opacity: 0.7 },
pressed: { y: 6, opacity: 0.9 },
springs: {
y: { stiffness: 500, damping: 100, mass: 3 },
opacity: { stiffness: 300, damping: 60 },
},
}
return (
<div
use:spring={CARD_ANIM[state()]}
on:mouseenter={state.set('hover')} {/* CallExpression → auto-thunked */}
on:mouseleave={state.set('rest')} {/* CallExpression → auto-thunked */}
on:mousedown={state.set('pressed')} {/* CallExpression → auto-thunked */}
on:mouseup={state.set('hover')} {/* CallExpression → auto-thunked */}
class="size-3 rounded-full bg-black cursor-pointer"
/>
)
}

Every event handler is one expression. No () => boilerplate. The four on: lines read top-to-bottom as a natural state-transition table — exactly the four edges of the state machine.

Each state.set(...) is a CallExpression, so phaze-compile wraps it in () => state.set(...) at build time; the runtime sees a normal onMouseenter={fn} JSX attribute and wires it via the standard addEventListener path. use:spring={CARD_ANIM[state()]} picks up the per-channel physics from the nested springs key in the same record (auto-fuse — see the use:spring directive contract below). The whole interaction lives in one record + one component body.