# How to: two-way bind a value

> Vue v-model, Angular [(value)], and Svelte $bindable() on controlled components.

You want a native control's value to stay in sync with a variable without
wiring your own change handler by hand.

<Tabs syncKey="framework">
  <TabItem label="React">
    React has no equivalent sugar. React's controlled-component idiom (`value` +
    `onValueChange`, both explicit) is already the two-way binding — there is no
    `[(value)]`/`v-model` shorthand to reach for, and none is planned.
  </TabItem>
  <TabItem label="Vue">
    This is Vue's `v-model` sugar, and it works on `switch`/`text-input` even though
    they're bare tags now, not components: Vue's compiler emits its own runtime
    `vModelText` directive for any element it doesn't recognize as a DOM input, and the
    adapter's `runtime-helpers` shim overrides it just enough to write each primitive's
    real value type (`Switch` needs a real boolean, not `vModelText`'s stringified
    default). `Slider` is unrelated to this mechanism — it's a real component, from the
    separate [`@symbiote-native/slider`](/docs/packages/slider/) wrapper package
    (`@symbiote-native/slider/vue`):

    ```vue
    <script setup lang="ts">
    import { Slider } from '@symbiote-native/slider/vue';
    </script>

    <template>
      <switch v-model="enabled" />
      <text-input v-model="name" />
      <Slider v-model="volume" :minimum-value="0" :maximum-value="1" />
    </template>
    ```

    Bare `v-model="x"` binds to `onUpdate:modelValue`; named `v-model:value="x"` binds to
    `onUpdate:value`. Each primitive's behavior resolves whichever arrived and reports
    both, so `:value`/`@value-change` and `v-model` are interchangeable, never a choice
    you have to commit to upfront.

  </TabItem>
  <TabItem label="Angular">
    This is Angular's banana-in-a-box `[(value)]` two-way binding. `switch` and
    `text-input` are element directives now (not components) but still declare a
    matching `value` `@Input()` + `valueChange` `@Output()`, so `[(value)]` sugar
    works unmodified on the tag — `Slider` is the one real component here, from the
    separate `@symbiote-native/slider/angular` package:

    ```ts
    import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';
    import { Slider } from '@symbiote-native/slider/angular';

    // component: imports: [SYMBIOTE_ELEMENTS, Slider]
    // template: `
    //   <switch [(value)]="enabled" />
    //   <text-input [(value)]="name" />
    //   <Slider [(value)]="volume" [minimumValue]="0" [maximumValue]="1" />
    // `
    ```

    This is not a special case wired per-primitive — it falls out of the RN prop
    name (`value`) already matching Angular's `@Output()` naming convention
    (`value` + `valueChange`), which is exactly what `[(x)]` expects.

  </TabItem>
  <TabItem label="Svelte">
    `switch` and `text-input` are bare tags now, not components — and Svelte's
    `$bindable()` mechanism only exists inside a component's own `$props()`, so
    there's nothing left to declare it on. `bind:value` doesn't even compile on a
    plain tag (Svelte only recognizes `bind:` on a handful of real DOM elements or
    a component that opts in), so two-way binding on these two is explicit `value` +
    `onValueChange` — Svelte's own equivalent of React's controlled-component idiom:

    ```svelte
    <script lang="ts">
      let enabled = $state(false);
      let name = $state('');
    </script>

    <switch value={enabled} onValueChange={(next) => (enabled = next)} />
    <text-input value={name} onValueChange={(next) => (name = next)} />
    ```

    `Slider` is different: it's a real component from the separate
    [`@symbiote-native/slider`](/docs/packages/slider/) wrapper package
    (`@symbiote-native/slider/svelte`) and still declares `value = $bindable()`, so
    `bind:value` keeps working there:

    ```svelte
    <script lang="ts">
      import { Slider } from '@symbiote-native/slider/svelte';

      let volume = $state(0.5);
    </script>

    <Slider bind:value={volume} minimumValue={0} maximumValue={1} />
    ```

    That caveat is specific to `$bindable()` and so only applies to `Slider` now:
    supplying your own `onValueChange` on the same instance as `bind:value` silently
    takes over — the component can't tell whether an instance is bound from the
    inside, so it only echoes the native-reported value back into your bound
    variable when `onValueChange` is absent. Pass one or the other, not both, on the
    same control.

  </TabItem>
  <TabItem label="Solid">
    Solid has no two-way syntax either: no `v-model`, no `$bindable()`. A signal's
    getter/setter pair already IS the two halves, so the controlled-component idiom is
    the same flat `value` + `onValueChange` bag React uses, just fed by a signal instead
    of a hook:

    ```tsx
    import { createSignal } from 'solid-js';
    import { Slider } from '@symbiote-native/slider/solid';

    const [enabled, setEnabled] = createSignal(false);
    const [name, setName] = createSignal('');
    const [volume, setVolume] = createSignal(0.5);

    <switch value={enabled()} onValueChange={setEnabled} />
    <text-input value={name()} onValueChange={setName} />
    <Slider value={volume()} onValueChange={setVolume} minimumValue={0} maximumValue={1} />
    ```

    `setEnabled`/`setName`/`setVolume` are handed straight to `onValueChange`: a signal
    setter already takes the new value as its one argument, so there's no wrapper to write.

  </TabItem>
</Tabs>

## Why this isn't fragile

The controlled-write correction (native reports a change, you don't update
`value`, SymbioteNative commands native back to the JS value) is shared logic in
`@symbiote-native/components`, not reimplemented per adapter or per binding style —
`v-model`/`[(value)]`/`$bindable()` are all sugar on top of the same contract
described in the [Events guide](/docs/learn/events/#controlled-components) and
the [Vue API reference](/docs/api/vue/#model-bindings-v-model).
