# How to: share content across surfaces

> createPortal/Teleport for same-surface content, createTunnel for cross-surface.

You want to render content somewhere other than its own JSX/template
position — a toast, an overlay — and need to know which mechanism applies.

## Same surface: `createPortal` (React) / `Teleport` (Vue) / `*portal` (Angular) / `Portal` (Svelte, Solid)

Both move content to an already-mounted node within the **same** mounted
surface as the call site:

<Tabs syncKey="framework">
  <TabItem label="React">
    ```tsx
    // React
    const [overlay, setOverlay] = useState<IHostInstance | null>(null);
    // ...
    <view ref={setOverlay} />
    {overlay ? createPortal(<text>ported in</text>, overlay) : null}
    ```
  </TabItem>
  <TabItem label="Vue">
    ```vue
    <!-- Vue -->
    <Teleport v-if="toastVisible && overlayHost" :to="overlayHost">
      <text>Ported via Teleport</text>
    </Teleport>
    <view ref="overlayHost" />
    ```
  </TabItem>
  <TabItem label="Angular">
    Angular has no runtime component synthesis (no JIT under Metro/AOT), so `createPortal`
    isn't a factory here — `*portal` is a structural directive instead, the same idiom as
    `*ngIf`, paired with a `portalOutlet` marking the destination. The primitive tags need
    `imports: [SYMBIOTE_ELEMENTS]` on the component, same as any other Angular template:

    ```html
    <!-- Angular -->
    <view portalOutlet #overlayHost="portalOutlet"></view>
    @if (toastVisible) {
      <view *portal="overlayHost"><text>Ported via *portal</text></view>
    }
    ```

  </TabItem>
  <TabItem label="Solid">
    Solid spells it `<Portal mount={...}>`, not a `createPortal()` call, because JSX in Solid
    evaluates eagerly at the position it's written, so a `createPortal(children, target)` call
    would build the toast's nodes before anything decided whether to portal them. `children` is
    the only lazily-evaluated shape Solid has, which is what a component prop gives you:

    ```tsx
    // Solid
    const [overlay, setOverlay] = createSignal<ISymbioteNode | null>(null);
    // ...
    <view ref={setOverlay} />
    {overlay() ? <Portal mount={overlay()}><text>ported in</text></Portal> : null}
    ```

  </TabItem>
  <TabItem label="Svelte">
    Svelte has no same-surface portal primitive at all — not even a partial one.
    `createPortal` is react-reconciler's own Fiber-level `HostPortal`, and Vue's `Teleport` is a
    separate Vue-runtime relocation feature; neither has anything for a framework with no
    reconciler to hook into, and there's no `*portal`-style directive to reach for either. Skip
    straight to `createTunnel` below — it's the only cross-content primitive this adapter has,
    and it happens to work same-surface too.
  </TabItem>
</Tabs>

Use a state/ref callback (`useState`, not `useRef`, on React) so the target
resolves once it actually commits — a plain ref is `null` on the entire first
render.

<Aside type="caution" title="Same-surface only, permanently">
  Stock React Native has no portal at all (Fabric's persistent host config never
  implements the mutation-mode container ops it needs). SymbioteNative's React
  adapter runs in mutation mode, so `createPortal` structurally works — but only
  within the surface that owns the commit. A target in a second,
  independently-mounted surface silently never repaints; this is not a gap to
  eventually close, see `createTunnel` below instead.
</Aside>

## Cross surface: `createTunnel`

For content that must reach a genuinely different, separately-mounted
surface, use `createTunnel()` instead — a shared store, not a node reference:

<Tabs syncKey="framework">
  <TabItem label="React">
    ```tsx
    // React — module-level singleton, importable from any surface
    export const overlayTunnel = createTunnel();

    // wherever it should paint:
    <overlayTunnel.Out />
    // wherever the content originates, any surface:
    <overlayTunnel.In><ToastCard /></overlayTunnel.In>
    ```

  </TabItem>
  <TabItem label="Vue">
    ```vue
    <!-- Vue -->
    <tunnel.Out />
    <tunnel.In><text>Ported via createTunnel</text></tunnel.In>
    ```
  </TabItem>
  <TabItem label="Angular">
    Same no-JIT constraint as `*portal` above — `TunnelInDirective`/`TunnelOut` are one static,
    pre-authored pair parameterized by the store `createTunnel()` returns, not a per-call
    factory output:

    ```ts
    // Angular — module-level singleton, importable from any component
    export const overlayTunnel = createTunnel();
    ```

    ```html
    @if (toastVisible) {
      <view *tunnelIn="overlayTunnel"><text>Ported via createTunnel</text></view>
    }
    <tunnel-out [tunnel]="overlayTunnel" />
    ```

  </TabItem>
  <TabItem label="Solid">
    ```tsx
    // Solid - module-level singleton, importable from any surface
    export const overlayTunnel = createTunnel();

    // wherever it should paint:
    <overlayTunnel.Out />
    // wherever the content originates, any surface:
    <overlayTunnel.In><ToastCard /></overlayTunnel.In>
    ```

    Same shape as React and Vue: Solid components are ordinary functions, so `createTunnel()` can
    mint a fresh `In`/`Out` pair per call, closed over that tunnel's own registry.

  </TabItem>
  <TabItem label="Svelte">
    ```svelte
    <!-- Svelte — module-level singleton, importable from any surface -->
    <script lang="ts" module>
      import { createTunnel } from '@symbiote-native/svelte';

      export const overlayTunnel = createTunnel();
    </script>
    ```

    ```svelte
    <!-- wherever it should paint: -->
    <TunnelOut tunnel={overlayTunnel} />

    <!-- wherever the content originates, any surface: -->
    {#if toastVisible}
      <TunnelIn tunnel={overlayTunnel}>
        {#snippet children()}<ToastCard />{/snippet}
      </TunnelIn>
    {/if}
    ```

    `TunnelIn`/`TunnelOut` take the tunnel as an explicit `tunnel` prop instead of
    `tunnel.In`/`tunnel.Out` — a `.svelte` file compiles to one fixed, top-level component, so
    there's no per-call factory `createTunnel()` could bake a registry into via closure the way
    Vue's `defineComponent` can.

  </TabItem>
</Tabs>

`In`/`Out` are components, not hooks/composables — an earlier hook-based
React version caused a genuine infinite render loop (the shared store's
`notify()` re-rendering the same component that also called the write side).
As separate components, `Out`'s forced re-render never bounces back into
`In`, even when they're siblings.

## Picking one

- Overlay host lives in the same tree you're already rendering → `createPortal`
  / `Teleport`.
- Content needs to reach a different `mount()` root entirely (split-screen, an
  always-on-top system surface) → `createTunnel`.

Import both from your adapter's package (`@symbiote-native/react`, `@symbiote-native/vue`,
`@symbiote-native/angular`, `@symbiote-native/svelte`, `@symbiote-native/solid`). Angular exposes the
same two mechanisms as directives instead of components/hooks: `*portal`/`*tunnelIn` and
`<tunnel-out>`. Solid has both mechanisms too, spelled `<Portal mount={...}>` and `createTunnel()`'s
`In`/`Out`; same-surface portal now ships on every adapter except Svelte, which exposes only the
second mechanism, `createTunnel`, as `TunnelIn`/`TunnelOut` components taking an explicit `tunnel`
prop.
