What happens
A native <Switch> renders as a real UISwitch but is completely inert. Tapping it does not move the thumb, and @change / onValueChange never reaches the script.
The thumb not moving is the part I'd look at first: a UISwitch toggles itself on touch regardless of what targets are attached, so this looks less like the event failing to cross the bridge and more like the switch never receiving the touch at all.
Repro
A native screen (renderer: 'native', the path in nativeScreens) with four controls side by side. Tap each and watch the counter:
<script client lang="ts">
const probe = state<number>(0)
function hit(n: number): void { probe.set(n) }
</script>
<template>
<View class="flex-1 bg-[#f2f2f7]">
<Text :text="'probe=' + probe()" />
<Switch id="p-switch" @change="hit(1)" />
<Checkbox id="p-checkbox" @change="hit(2)" label="checkbox" />
<Slider id="p-slider" @change="hit(3)" :minimumValue="0" :maximumValue="5" />
<TextInput id="p-input" @input="hit(4)" placeholder="input" />
</View>
</template>| control | event | fires? |
|---|---|---|
Switch | @change → onValueChange | no |
Checkbox | @change → onValueChange | yes |
Slider | @change → onValueChange | yes |
TextInput | @input → onChangeText | yes |
Pressable | @press → onPress | yes |
Same screen, same build, same tap harness. So this is specific to Switch, not to onValueChange and not to event dispatch generally.
What I ruled out
- Event naming. The compiled bundle emits
type:"Switch"withevents:{onValueChange:"hit(1)"}— exactly the keyCraftNativeScreen.swiftreads (updateHandler(nonEmptyHandler(events["onValueChange"]) ?? nonEmptyHandler(events["onChange"]), for: toggle)), andmakeViewdoestoggle.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged). Both sides look right. - Zero-size hit area. An explicit
class="h-[31px] w-[51px]"(the intrinsic size) behaves identically to no sizing at all. - Controlled-prop snapback. I first suspected
:valuewas being re-applied over the user's change. It is not the cause: aSwitchwith no:valueand no handler-side state is equally inert, and the thumb does not move even momentarily. - The handler itself. Pointing
@changeat a function that demonstrably fires from aPressableon the same screen still did nothing.
Why it matters
There is no way to draw a working toggle on a native screen. A UISwitch that animates nothing and changes nothing reads to a user as a control that silently forgets what they set — worse than one that is visibly disabled. The workaround is a Pressable row with a checkmark and accessibilityRole="switch", which works but is not the control the platform expects for a binary setting.
Environment
craft-native0.0.187stacks0.75.187,@stacksjs/stx0.2.176- iPhone 17 Pro simulator, iOS 26.5
- Xcode 26.6 (17F113), built with
CODE_SIGN_IDENTITY=-