# Events

> Understand how native events surface as React callbacks, Vue emits/v-model, Angular bindings, Svelte callback props, and Solid callback props.

Native Fabric events come back to the engine. Each adapter then exposes them in
that framework's idiom: React callback props, Vue typed emits (plus `v-model`
on controlled components), Angular real `@Output()` `EventEmitter`s, and
Svelte and Solid plain callback props — the DOM-shim adapter and the Solid
renderer both pass the same event object straight through, with no
framework-specific wrapper or emit translation — with one permanent
exception, the scroll-family events, which stay callback inputs (see the
caveat below).

## Same event, different framework shape

<Tabs syncKey="framework">
  <TabItem label="React">
    ```tsx
    <switch value={enabled} onValueChange={setEnabled} />
    <text-input value={name} onValueChange={setName} />
    <pressable onPress={() => setCount(count + 1)} />
    ```
  </TabItem>
  <TabItem label="Vue">
    Vue uses typed emits, or `v-model` on the controlled components that support it:

    ```vue
    <switch v-model="enabled" />
    <text-input v-model="name" />
    <pressable @press="count++" />
    ```

  </TabItem>
  <TabItem label="Angular">
    Angular uses real `@Output()` `EventEmitter`s everywhere, including `Switch` and
    `TextInput`:

    ```ts
    template: `
      <switch [value]="enabled" (valueChange)="setEnabled($event)" />
      <text-input [value]="name" (valueChange)="setName($event)" />
      <pressable (press)="count = count + 1" />
    `
    ```

  </TabItem>
  <TabItem label="Svelte">
    Svelte receives the same native event object shape as React — props pass through
    as-is, no framework-specific event wrapper:

    ```svelte
    <switch value={enabled} onValueChange={next => (enabled = next)} />
    <text-input value={name} onValueChange={next => (name = next)} />
    <pressable onPress={() => (count += 1)} />
    ```

  </TabItem>
  <TabItem label="Solid">
    Solid receives the same native event object shape as React and Svelte,
    props pass through as-is with no framework-specific event wrapper:

    ```tsx
    <switch value={enabled()} onValueChange={next => setEnabled(next)} />
    <text-input value={name()} onValueChange={next => setName(next)} />
    <pressable onPress={() => setCount(count() + 1)} />
    ```

  </TabItem>
</Tabs>

The payload is the same concept. The public API is framework-shaped.

<Aside type="caution" title="Scroll events stay callback inputs">
  The scroll-family events (`onScroll`, `onScrollBeginDrag`, `onScrollEndDrag`,
  `onMomentumScrollBegin`, `onMomentumScrollEnd`) are the one **permanent**
  exception to Angular's `@Output()` migration, on `ScrollView` and the list
  components. They stay plain callback `@Input()`s — `[onScroll]="handler"` —
  because RN's scroll callbacks can carry either a plain function or the return
  value of `Animated.event(...)`, a native-driver marker for native-driven
  scroll animations, and `@Output()` can only bind a template listener
  expression, not an arbitrary value. See the [Angular API
  reference](/docs/api/angular/) for the exact split.
</Aside>

## Passthrough events vs adapter events

Some events are native passthrough events. They travel through the engine as
`onX` listeners and should not be declared as Vue emits unless the Vue component
actually consumes and re-emits them.

Other events are adapter-level conveniences. Examples:

| Concept              | React                         | Vue                               | Angular                      | Svelte                        | Solid                         |
| -------------------- | ----------------------------- | --------------------------------- | ---------------------------- | ----------------------------- | ----------------------------- |
| Press                | `onPress`                     | `@press`                          | `(press)`                    | `onPress`                     | `onPress`                     |
| Switch value         | `onValueChange(value, event)` | `@value-change`<br />or `v-model` | `(valueChange)` (value only) | `onValueChange(value, event)` | `onValueChange(value, event)` |
| Text input text      | `onValueChange(text, event)`  | `@value-change`<br />or `v-model` | `(valueChange)` (text only)  | `onValueChange(text, event)`  | `onValueChange(text, event)`  |
| Text input raw event | _(same callback, 2nd arg)_    | _(same emit, 2nd arg)_            | `(change)` (separate output) | _(same callback, 2nd arg)_    | _(same callback, 2nd arg)_    |

React, Vue, Svelte, and Solid merged `TextInput`'s old `onChangeText`/`onChange`
pair into one `onValueChange(text, event)` callback — there's only ever one
underlying native event. Angular keeps two separate `@Output()`s instead,
because an `EventEmitter` carries exactly one value: `valueChange` stays
text-only (so `[(value)]` two-way binding keeps working) and `change` is a
second output for the raw event. `Switch` follows the same shape one level
simpler: React's, Svelte's, and Solid's `onValueChange`, and Vue's
`@value-change`, all receive the native event as a second argument, but
Angular's `valueChange` stays single-value (just the boolean) since there's
no second output to split it into.

This split matters because Vue removes declared emit listeners from attrs. If a
native passthrough listener is declared as an emit by mistake, the engine may
never see it.

## Controlled components

`Switch` and `TextInput` follow React Native's controlled-component rules. If the
native view reports a new value but the parent does not update the `value` prop,
SymbioteNative commands native back to the JavaScript value.

That correction logic is shared in `@symbiote-native/components`; React, Vue,
Angular, Svelte, and Solid each only wire it to their own lifecycle systems.
