# Vue API

> The Vue adapter public contract.

Primitives (`view`, `text`, `pressable`, `text-input`, `scroll-view`, ...) are
plain lowercase intrinsic tags — write them directly in a template or JSX, no
import needed. Import only what's still a real component or module from
`@symbiote-native/vue`:

```ts
import { StyleSheet } from '@symbiote-native/vue';
```

Vue drives the same engine as React, but its public API follows Vue conventions.
That matters most for events, children, and refs.

## Installation

```sh
pnpm add @symbiote-native/vue react-native vue
```

`react-native` and `vue` stay your app's own top-level dependencies — this
package replaces only the JS renderer that drives them, and `react-native` is
the Metro version anchor, so it has to be pinned at the app root.
`@symbiote-native/engine` is a peer dependency and installs alongside. TSX needs
nothing beyond this; SFC additionally needs the Metro transformer for `.vue`
files.

## Component shape

- Events: attrs listeners like `@press` and `@value-change`, routed straight
  to the tag — same as any native DOM event in Vue.
- Children: default slots.
- Render children: list components only (`FlatList`, `SectionList`, ...) still
  take a scoped slot; a primitive tag like `pressable` has no component body
  to call one against — see [Slots](#slots) below for the pressed-state
  workaround.
- Refs: template refs to host nodes or exposed component handles.
- Styles: `:style="styles.root"` with React Native-style objects. Most
  visual components — not just `View`/`Text` anymore — also accept
  `class`/`:class` for a registered class name, resolved through the same
  style registry as a Vue SFC `<style>` block, a Svelte component's own
  `<style>` block, or a `.module.css` import; see the [Styling
  guide](/docs/learn/styling/).
- Attribute names: templates may use kebab-case; the adapter normalizes to
  camelCase.

## Common events

```vue
<pressable @press="onPress" @long-press="onLongPress" />
<switch v-model="enabled" />
<text-input v-model="text" @value-change="(text, event) => onChange(event)" />
<view @layout="onLayout" />
```

Vue event names are kebab-cased in templates. Their TypeScript source names use
camelCase emits such as `valueChange`, shared by `Switch` (`boolean` payload)
and `TextInput` (`(text, event)` payload — the merge of what used to be
separate `changeText`/`change` emits). `Switch` and `TextInput` also accept
the explicit `:value`/`@value-change` form — `v-model` is sugar over the same
pair, see [model bindings](#model-bindings-v-model) below.

## Slots

`pressable` is a plain tag, so pressed state no longer arrives as a scoped
slot — wire the two press events into local state instead:

```vue
<script setup lang="ts">
import { ref } from 'vue';

const pressed = ref(false);
</script>

<template>
  <pressable @press-in="pressed = true" @press-out="pressed = false">
    <text>{{ pressed ? 'Release' : 'Press me' }}</text>
  </pressable>
</template>
```

List renderers still take a Vue slot or render function for their cell, since
`renderItem` returns a framework element and stays a real component
(`FlatList`, `SectionList`, `VirtualizedList`, `VirtualizedSectionList`).

## Refs and handles

Host node refs should be treated as native handles, not DOM elements. Composed
components expose their own imperative handles through Vue's `expose()`.

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

const input = ref<ITextInputHandle | null>(null);
</script>

<template>
  <text-input ref="input" value="" @value-change="() => {}" />
</template>
```

## Model bindings (`v-model`)

`Switch`, `TextInput`, and the [Slider](/docs/packages/slider/) wrapper accept
`v-model` on top of their existing `value`/`@change-*` contract:

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

Bare `v-model="x"` compiles to prop `modelValue` + emit `update:modelValue`;
named `v-model:value="x"` compiles to prop `value` + emit `update:value`. Each
model-capable component resolves whichever prop arrived (`modelValue` first,
falling back to `value`) and fires both update events, so either compiler
target — and the original explicit `value`/`@value-change` pair — works
interchangeably. `resolveModelValue`/`emitModelUpdate` (exported from
`@symbiote-native/vue`) are the shared helpers behind this, for wrapper authors adding
`v-model` to their own controlled component.

## `v-show`

`v-show` works on any SymbioteNative host node without extra setup — it toggles the
node's native `style.display` between its resolved value and `'none'` instead
of unmounting, matching Vue's DOM `v-show`:

```vue
<view v-show="visible">
  <text>Stays mounted, just hidden</text>
</view>
```

Unlike `v-if`, state under a `v-show="false"` node survives a hide/show
round-trip because the subtree is never torn down.

## Runtime modules

`@symbiote-native/vue` re-exports the same runtime utilities as the other adapters,
so app code keeps one import root: `Platform`, `StyleSheet`, `Dimensions`,
`PixelRatio`, `PlatformColor`, `DynamicColorIOS`, `Alert`, `Share`, `Linking`,
`Keyboard`, `Vibration`, `ActionSheetIOS`, `BackHandler`, `ToastAndroid`,
`PermissionsAndroid`, `AccessibilityInfo`, `I18nManager`, `Settings`,
`LayoutAnimation`, `InteractionManager`, `StatusBar`, plus the
`useWindowDimensions`/`useColorScheme` composables, `findNodeHandle`, and
`dlog`/`isDebug` for diagnostic logging.

On Android, `Keyboard` and `Settings` do nothing until you also install
[`@symbiote-native/android`](/docs/packages/android/):
`keyboardDidShow`/`keyboardDidHide` never fire, because Android's only stock
keyboard-event source is `ReactRootView`'s layout listener and the bridgeless
surface SymbioteNative mounts never triggers it, and `Settings` reads back `null`,
because RN's `Settings` has no stock Android implementation at all. That package
supplies both native modules (`KeyboardObserver`, `SettingsManager`), is picked
up by Gradle autolinking, and exports nothing from JS — installing it is the
whole integration.

## Animations and gestures

`Animated` (both the JS and native driver) and `PanResponder` are re-exported
from `@symbiote-native/vue` — see the [Animations guide](/docs/learn/animations/)
for the full surface and per-driver tradeoffs.

## Portals (`Teleport`)

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

const overlayHost = shallowRef<ISymbioteNode | null>(null);
</script>

<template>
  <view ref="overlayHost" />
  <Teleport :to="overlayHost" v-if="overlayHost">
    <text>Rendered under overlayHost, not here</text>
  </Teleport>
</template>
```

`Teleport` renders its slot content under a different node than its own
template position — the same primitive as React's `createPortal`. Vue's
`Teleport` normally resolves a string `to` (`to="body"`, `to="#modal-root"`)
through `querySelector`, which doesn't exist in a non-DOM renderer, so `to`
here must be an already-mounted host node **ref**, not a selector string — a
CSS-selector string or anything that isn't a real rendered node throws
immediately, rather than silently corrupting the tree. Same **v1 scope** as
React's portal: the target must live in the same surface as the `<Teleport>`
call site.

## Cross-surface content (`createTunnel`)

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

const overlayTunnel = createTunnel(); // module-level singleton, importable from both surfaces
</script>

<template>
  <!-- inside the surface that should paint the content -->
  <view :style="styles.overlayHost">
    <overlayTunnel.Out />
  </view>

  <!-- inside any other component, in any surface -->
  <overlayTunnel.In v-if="toastVisible">
    <ToastCard />
  </overlayTunnel.In>
</template>
```

`Teleport` only reaches a target in the _same_ surface. `createTunnel` is for
two independently `mount()`-ed surfaces that share no Fabric tree at all.
`In` and `Out` are separate components — not composables, since a composable
can't accept template markup — linked only by a small reactive store: `In`
registers its slot content wherever it renders, `Out` reads the store and
paints in whichever surface actually mounts it.

React, Angular, Svelte, and Solid all have the same primitive — see their own
API references.

## App entry point (`AppRegistry`)

`@symbiote-native/vue` exports the same `AppRegistry`/`setHostRegistrar` entry point
described in the [Core API](/docs/api/core/#app-entry-point-appregistry). One
Vue-specific detail: a component passed to `setWrapperComponentProvider`
receives the app root through its **default slot**, not as a `children`-like
argument — write it as an ordinary Vue component that renders `<slot />`:

```vue
<!-- ThemeWrapper.vue -->
<template>
  <ThemeProvider>
    <slot />
  </ThemeProvider>
</template>
```

```ts
import { AppRegistry } from '@symbiote-native/vue';
import ThemeWrapper from './ThemeWrapper.vue';

AppRegistry.setWrapperComponentProvider(() => ThemeWrapper);
AppRegistry.registerComponent('MyApp', () => App);
```

## Gotcha: emits vs passthrough listeners

Only adapter-level events should be declared as emits. Native passthrough
listeners must remain attrs so the engine can route them to Fabric. This is why
Vue API docs call out exactly which events are emits per component.
