ArenaSwitch.prompt.md

September 6, 2026 ยท View on GitHub

Controlled on/off switch showing an icon per state (iconOn/iconOff, Phosphor class strings, Arena draws the <i>). state is the CURRENT value; the host owns it and pushes it back on every render.

const [dark, setDark] = useState(false);

<ArenaSwitch state={dark} onFuncOn={() => setDark(true)} onFuncOff={() => setDark(false)}
  iconOn="ph-bold ph-moon" iconOff="ph-bold ph-sun" label="Dark theme" />

Members, in contract order and under this layer's own names. * marks a required one.

MemberFormTypeDefaultWhat it is
stateprimitivebooleanfalseThe current on/off value. Controlled: the consumer owns it and pushes it each render.
orientationenumArenaOrientation"horizontal"Whether the switch lies horizontally or stands vertically.
sizeenumArenaSwitchSize"md"The switch's overall size.
iconOnprimitivestringA Phosphor class name for the glyph shown while on. Arena draws the aria-hidden <i>.
iconOffprimitivestringA Phosphor class name for the glyph shown while off.
label*primitivestringThe accessible name for the switch, also drawn beside it.
disabledprimitivebooleanfalseWhether the switch is inoperable.
confirmprimitivebooleanfalseWhen set, a change is not applied on the fly; it is requested through requestChange so the host can confirm it first.
onFuncOneventThe switch was turned on.
onFuncOffeventThe switch was turned off.
onRequestChangeeventA change was requested while confirm is set: the host opens an ArenaConfirmDialog and, on confirmation, flips state (the requested value is always the negation of the current one).

onFuncOn/onFuncOff are transition events rather than a value; each fires with no payload, once, for the direction the activation moved. There is no onChange: read the direction from which handler fired, not from an event argument.

For high-impact toggles (H5) add confirm. An activate no longer fires onFuncOn or onFuncOff at all, and calls onRequestChange() instead. That call is payload-less too, since the requested value is always !state. The host can then open an ArenaConfirmDialog and push state itself once the user confirms. confirm alone is what diverts the activation, never whether a handler was passed: confirm set with no onRequestChange is a switch that does nothing at all. The cost is the accepted one of the rule that no render or behaviour follows from whether a listener is bound. The cost is worth paying, because what it replaced applied a guarded change silently. No runtime guard can catch it: "is anything listening?" is precisely the question a component may not ask. The behaviour is pinned, so the fallback cannot come back unnoticed.

const [armed, setArmed] = useState(false);
const [pending, setPending] = useState(false);

<ArenaSwitch label="Automatic deployment to production" state={armed} confirm
  onRequestChange={() => setPending(true)} />

<ArenaConfirmDialog open={pending} title="Enable automatic deployment"
  confirmLabel="Enable" onCancel={() => setPending(false)}
  onConfirm={() => { setArmed(!armed); setPending(false); }}>
  Every approved commit will be deployed to production without manual review.
</ArenaConfirmDialog>

orientation, which is 'horizontal' by default or 'vertical', lays the track along the other axis. Reach for vertical only where the surrounding layout is itself vertical, such as a narrow settings rail, and never as a decorative variant. size ('sm' | 'md' | 'lg' | 'xl' | '2xl', default 'md') scales the track and knob together; 'md' matches the pre-redesign component's only size exactly, so an existing call site that names no size renders unchanged.

Do own state in the parent and push it back from onFuncOn/onFuncOff (or from ArenaConfirmDialog's onConfirm when confirm is set), ArenaSwitch never changes its own value.

<ArenaSwitch state={notify} onFuncOn={() => setNotify(true)} onFuncOff={() => setNotify(false)} label="Notify on approval" />

Don't treat onFuncOn/onFuncOff as a replacement onChange(next): there is no payload, and reaching for e.target.checked or a boolean argument is reaching for a member this API does not have.

{/* Wrong: there is no event object and no boolean argument to read. */}
<ArenaSwitch state={notify} onFuncOn={(e) => setNotify(e.target.checked)} label="Notify on approval" />

Don't wire both confirm and onFuncOn/onFuncOff expecting the transition events to still fire once confirmed, they never do. While confirm is set, activation always routes through onRequestChange() alone; flip state from wherever the confirmation resolves (typically ArenaConfirmDialog's onConfirm), not from a transition event that confirm suppresses.

The rules of the language hold in the code you write from this page. An Arena component is not a styling surface, so put no className of your own on it. Read every value through its token, never a raw colour and never a bare 16px. Never wrap it in your router's own link. arena-to-prod --audit reports these three in your sources. The rest are in ../../../../../skills/design/SKILL.md, which marks the ones it reports.